diff --git a/.gitattributes b/.gitattributes index 12a665f563..19f3e8920a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -14,16 +14,12 @@ apps/cli/src/shared/telemetry/__fixtures__/error-tags.txt text eol=lf # statistics and collapses them in pull request diffs -- they are build output to # be regenerated, not code to review, and together they outweigh the hand-written # source. The attribute is GitHub-only: git, CI, and local diffs are unaffected. -# Collapsed is not hidden. v1-openapi.yaml and the lockfiles do carry changes -# worth reading -- an upstream API change, a dependency substitution -- and both -# stay one click away in review. -# Regeneration is owned by `go generate` in apps/cli-go and by the `generate` -# turbo task, whose `outputs` in turbo.json are the source of truth for this list. -# Only the generated files are listed, not their hand-written inputs -# (pkg/api/*.cfg.yaml, api/overlay.yaml). apps/docs/content/docs/commands/ is -# generated too but gitignored, so it needs no entry. -apps/cli-go/api/v1-openapi.yaml linguist-generated=true -apps/cli-go/pkg/api/*.gen.go linguist-generated=true +# Collapsed is not hidden. The lockfile does carry changes worth reading -- +# a dependency substitution -- and stays one click away in review. +# Regeneration is owned by the `generate` turbo task, whose `outputs` in +# turbo.json are the source of truth for this list. Only the generated files +# are listed, not their hand-written inputs. apps/docs/content/docs/commands/ +# is generated too but gitignored, so it needs no entry. packages/api/src/generated/* linguist-generated=true packages/api/scripts/openapi-source.json linguist-generated=true packages/stack/src/functions/generated/* text eol=lf linguist-generated=true @@ -33,7 +29,6 @@ apps/docs/public/schemas/** linguist-generated=true # Lockfiles. Collapsed rather than hidden -- a dependency change is still one # click away in review, which is the same treatment GitHub gives package-lock.json. pnpm-lock.yaml linguist-generated=true -apps/cli-go/go.sum linguist-generated=true # Not a language marker -- this is the service image manifest, not a container build. apps/cli/src/shared/services/Dockerfile linguist-detectable=false diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e2ad6c2359..e328847024 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -10,15 +10,8 @@ /tools/*/package.json @supabase/cli @supabase-oss /pnpm-lock.yaml @supabase/cli @supabase-oss /pnpm-workspace.yaml @supabase/cli @supabase-oss -/apps/cli-go/go.mod @supabase/cli @supabase-oss -/apps/cli-go/go.sum @supabase/cli @supabase-oss -/apps/cli-go/pkg/go.mod @supabase/cli @supabase-oss -/apps/cli-go/pkg/go.sum @supabase/cli @supabase-oss /apps/cli/src/shared/services/Dockerfile @supabase/cli @supabase-oss -/apps/cli-go/pkg/config/templates/Dockerfile @supabase/cli @supabase-oss /packages/stack/src/Artifacts.ts @supabase/cli @supabase-oss -/apps/cli-go/api/v1-openapi.yaml @supabase/cli @supabase-oss -/apps/cli-go/pkg/api/*.gen.go @supabase/cli @supabase-oss /packages/api/src/generated/ @supabase/cli @supabase-oss /packages/api/scripts/openapi-source.json @supabase/cli @supabase-oss diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 9304447079..29edac9a27 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -107,7 +107,7 @@ body: id: ticket-id attributes: label: Crash report ID - description: If the CLI printed one after rerunning with `--create-ticket`, paste the crash report ID. + description: If you have a crash report or support ticket ID, paste it here. placeholder: ab1ac733e31e4f928a4d7c8402543712 validations: required: false diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index eebbb5d160..d25fb9608b 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -14,16 +14,10 @@ inputs: fails with a path validation error. required: false default: "true" - go-cache: - description: >- - Whether to restore the Go module and build caches. Disable this in jobs - that never run Go. - required: false - default: "true" tools: description: >- Space-separated mise tools to install instead of every tool in - mise.toml. Skipping the Go toolchain keeps the restored mise cache small. + mise.toml. required: false default: "" pnpm-store-cache: @@ -188,41 +182,6 @@ runs: path: ${{ steps.pnpm-cache.outputs.path }}/lockfile-verified.jsonl key: pnpm-verify-${{ runner.os }}-${{ inputs.dependency-firewall-token != '' && 'firewall' || 'public' }}-${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml') }} - - name: Resolve Go cache paths - if: inputs.dependency-cache == 'true' && inputs.go-cache == 'true' - id: go-cache - shell: bash - run: | - echo "mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT" - echo "build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT" - - - name: Mount Go module disk - if: inputs.dependency-cache == 'true' && inputs.go-cache == 'true' && steps.sticky.outputs.enabled == 'true' - uses: useblacksmith/stickydisk@b6cb58d1d31d6d32a25248fab12493d831f7bd48 # v1.7.2 - with: - key: ${{ github.repository }}-go-mod-v3-${{ runner.arch }} - path: ${{ steps.go-cache.outputs.mod }} - commit: ${{ steps.sticky.outputs.commit }} - - - name: Mount Go build disk - if: inputs.dependency-cache == 'true' && inputs.go-cache == 'true' && steps.sticky.outputs.enabled == 'true' - uses: useblacksmith/stickydisk@b6cb58d1d31d6d32a25248fab12493d831f7bd48 # v1.7.2 - with: - key: ${{ github.repository }}-go-build-v3-${{ runner.arch }} - path: ${{ steps.go-cache.outputs.build }} - commit: ${{ steps.sticky.outputs.commit }} - - - name: Configure Go dependency cache - if: inputs.dependency-cache == 'true' && inputs.go-cache == 'true' && steps.sticky.outputs.enabled != 'true' - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - ${{ steps.go-cache.outputs.mod }} - ${{ steps.go-cache.outputs.build }} - key: go-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('apps/cli-go/go.mod', 'apps/cli-go/go.sum') }} - restore-keys: | - go-cache-${{ runner.os }}-${{ runner.arch }}- - - name: Install dependencies shell: bash env: diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b66df12186..c24da2c9cc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -37,28 +37,6 @@ updates: # — never bundle it into the actions-major group. cooldown: default-days: 7 - - package-ecosystem: "gomod" - directories: - - "/apps/cli-go" - - "/apps/cli-go/pkg" - schedule: - interval: "cron" - cronjob: "0 0 * * *" - commit-message: - prefix: "chore(cli-go): " - groups: - go-minor: - update-types: - - minor - - patch - exclude-patterns: - - github.com/compose-spec/compose-go/v2 - ignore: - - dependency-name: "github.com/compose-spec/compose-go/v2" - - dependency-name: "github.com/docker/cli" - - dependency-name: "github.com/docker/docker" - cooldown: - default-days: 7 - package-ecosystem: "npm" directory: "/" schedule: diff --git a/.github/workflows/build-cli-artifacts.yml b/.github/workflows/build-cli-artifacts.yml index 818f238b37..a66ca86419 100644 --- a/.github/workflows/build-cli-artifacts.yml +++ b/.github/workflows/build-cli-artifacts.yml @@ -71,10 +71,6 @@ jobs: with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} - - name: Pre-download Go modules - working-directory: apps/cli-go - run: go mod download -x - - name: Install nfpm env: NFPM_VERSION: "2.47.0" @@ -112,6 +108,7 @@ jobs: - name: Verify build artifacts run: | + set -euo pipefail for pkg in cli-darwin-arm64 cli-darwin-x64 cli-linux-arm64 cli-linux-arm64-musl cli-linux-x64 cli-linux-x64-musl cli-windows-arm64 cli-windows-x64; do echo "Checking packages/$pkg/bin/..." ls -la "packages/$pkg/bin/" @@ -119,6 +116,39 @@ jobs: echo "Checking dist/..." ls -la dist/ + # Every channel ships a single `supabase` binary; no `supabase-go` companion. + shopt -s nullglob + for f in packages/*/bin/supabase-go*; do + echo "::error::${f} is a supabase-go companion binary" + exit 1 + done + + check_listing() { + local artifact="$1" + shift + local listing + if ! listing="$("$@")"; then + echo "::error::failed to list the contents of ${artifact}" + exit 1 + fi + if grep -q 'supabase-go' <<<"$listing"; then + echo "::error::${artifact} contains supabase-go" + exit 1 + fi + } + + for a in dist/*.tar.gz dist/*.apk; do check_listing "$a" tar -tzf "$a"; done + for a in dist/*.zip; do check_listing "$a" unzip -Z1 "$a"; done + for a in dist/*.deb; do check_listing "$a" dpkg-deb -c "$a"; done + rpms=(dist/*.rpm) + if [[ ${#rpms[@]} -gt 0 ]]; then + if command -v rpm >/dev/null; then + for a in "${rpms[@]}"; do check_listing "$a" rpm -qlp "$a"; done + else + echo "::notice::rpm is not installed on this runner; rpm contents were not checked for supabase-go" + fi + fi + - name: Check existing build artifacts cache id: build-artifacts-cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 diff --git a/.github/workflows/cli-go-api-sync.yml b/.github/workflows/cli-go-api-sync.yml deleted file mode 100644 index 9ab33fd002..0000000000 --- a/.github/workflows/cli-go-api-sync.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: API Sync - -on: - repository_dispatch: - types: - - api-sync - # Fallback for a lost dispatch, per docs/openapi-sync.md. This matters more - # now that the per-pull-request Codegen check reads the committed snapshot: - # this workflow is the only thing that looks at the live specification, so a - # dropped dispatch would otherwise leave drift undetected indefinitely. - schedule: - - cron: "17 6 * * *" - workflow_dispatch: # allow manual triggering - -permissions: - contents: read - -jobs: - sync: - name: Sync API Types - runs-on: blacksmith-2vcpu-ubuntu-2404 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: apps/cli-go/go.mod - cache: true - - # The only place that reads the live spec. Every other consumer, including - # the per-PR Codegen check, regenerates from the committed snapshot, so - # upstream drift turns into this one pull request instead of a failure on - # every open pull request that touches apps/cli-go. - - name: Refresh the OpenAPI snapshot - run: curl -fsSL "$API_SPEC_URL" -o api/v1-openapi.yaml - env: - API_SPEC_URL: https://api.supabase.green/api/v1-yaml - - - name: Run codegen - run: go generate - - # Covers the snapshot as well as the generated client: an upstream edit - # that codegen ignores (a description, an example) would otherwise be - # refetched and discarded on every run, leaving the snapshot stale. The - # snapshot is compared byte-for-byte for the same reason -- under - # --ignore-space-at-eol a trailing-whitespace-only upstream edit would be - # refetched and discarded forever, which is the loop this check prevents. - # pkg keeps the flag: it is oapi-codegen output, not upstream bytes. - - name: Check for changes - id: check - run: | - if git diff --exit-code --quiet api/v1-openapi.yaml && - git diff --ignore-space-at-eol --exit-code --quiet pkg; then - echo "No changes detected" - echo "has_changes=false" >> "$GITHUB_OUTPUT" - else - echo "Changes detected" - echo "has_changes=true" >> "$GITHUB_OUTPUT" - fi - - - name: Generate token - id: app-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ vars.GH_APP_CLIENT_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - permission-pull-requests: write - permission-contents: write - - - name: Create Pull Request - if: steps.check.outputs.has_changes == 'true' - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 - with: - token: ${{ steps.app-token.outputs.token }} - commit-message: "chore: sync API types from infrastructure" - title: "chore: sync API types from infrastructure" - body: | - This PR was automatically created to sync API types from the infrastructure repository. - - Changes were detected after refreshing `apps/cli-go/api/v1-openapi.yaml` from the latest spec from infrastructure, and `apps/cli-go/pkg/api` is regenerated from it. The snapshot diff is the upstream API change and is worth reading: it is marked `linguist-generated`, so GitHub collapses it behind a "Load diff" click rather than hiding it. - branch: sync/api-types - base: develop -defaults: - run: - working-directory: apps/cli-go diff --git a/.github/workflows/cli-go-ci.yml b/.github/workflows/cli-go-ci.yml deleted file mode 100644 index 6ecd297cf5..0000000000 --- a/.github/workflows/cli-go-ci.yml +++ /dev/null @@ -1,137 +0,0 @@ -name: CI - -on: - pull_request: - paths: - - apps/cli-go/** - merge_group: - push: - branches: - - develop - paths: - - apps/cli-go/** - -permissions: - contents: read - -jobs: - test: - name: Test - runs-on: blacksmith-8vcpu-ubuntu-2404 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: apps/cli-go/go.mod - cache: true - - # Required by: internal/utils/credentials/keyring_test.go - - uses: t1m0thyj/unlock-keyring@cbcf205c879ebd86add70bab3a6abfcce59a5cae # v1.2.0 - - run: | - set -o pipefail - pkgs=$(go list -C pkg ./... | grep -Ev 'pkg/api' | paste -sd ',' -) - go tool gotestsum -- -race -v -count=1 ./... \ - -coverpkg="./cmd/...,./internal/...,${pkgs}" -coverprofile=coverage.out - - # internal/start (docker-compose/v2, buildx, moby/buildkit, k8s client-go, - # aws-sdk-go-v2, notary, secret-detector) was deleted outright as - # unreachable (CLI-1966) -- this guards against silently reintroducing - # that dependency tree via some other package, since nothing else checks - # the shipped binary's size on every change. - - name: Verify the heavy orchestration dependency tree stays out of the module - run: | - set -euo pipefail - go list -deps . > /tmp/deps.txt - ! grep -Eq \ - 'docker/compose|docker/buildx|moby/buildkit|k8s\.io/client-go|aws-sdk-go-v2' \ - /tmp/deps.txt - - - name: Check existing coverage cache - id: coverage-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: apps/cli-go/coverage.out - key: cli-go-coverage-${{ github.run_id }}-v1 - lookup-only: true - - - name: Save coverage cache - if: steps.coverage-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: apps/cli-go/coverage.out - key: cli-go-coverage-${{ github.run_id }}-v1 - - coverage: - name: Coverage - needs: - - test - runs-on: blacksmith-8vcpu-ubuntu-2404 - steps: - - name: Restore coverage cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: apps/cli-go/coverage.out - key: cli-go-coverage-${{ github.run_id }}-v1 - fail-on-cache-miss: true - - name: Move coverage report - run: cp apps/cli-go/coverage.out coverage.out - working-directory: . - - uses: coverallsapp/github-action@8d6379e14d29928660c4ba802d8e85393440b329 # v2.3.8 - with: - file: coverage.out - format: golang - - lint: - name: Lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - uses: jdx/mise-action@8418fedb1110381a0d3ba51a7a415d7c1b884550 # v5.2.2 - with: - version: 2026.10.17 - install_args: >- - go - golangci-lint - - - uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 - with: - args: --timeout 5m --verbose - install-mode: none - only-new-issues: true - working-directory: apps/cli-go - - codegen: - name: Codegen - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: apps/cli-go/go.mod - cache: true - - # Regenerates from the committed api/v1-openapi.yaml snapshot, so this - # check is hermetic: it fails only when a pull request leaves pkg/api out - # of sync with the snapshot it was generated from. It deliberately does - # not compare against the live upstream spec -- doing so made unrelated - # pull requests fail together whenever staging drifted ahead of the - # committed snapshot. That drift is the API Sync workflow's job. - - run: go generate - - run: | - if ! git diff --ignore-space-at-eol --exit-code --quiet pkg; then - echo "Detected uncommitted changes after codegen. See status below:" - git diff - exit 1 - fi -defaults: - run: - working-directory: apps/cli-go diff --git a/.github/workflows/cli-go-mirror.yml b/.github/workflows/cli-go-mirror.yml deleted file mode 100644 index a1f9f3a33a..0000000000 --- a/.github/workflows/cli-go-mirror.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Mirror Dependencies -# We mirror upstream container images like Migra, imgproxy, etc. because these -# are usually only available on certain image registry and not others (e.g. only -# on Docker Hub and not on ghcr.io or AWS ECR). -# -# For container images that we control, we usually publish to Docker Hub, -# ghcr.io, and AWS ECR. - -on: - # This workflow is the manual/bulk entry point for re-mirroring everything. - # Template image bumps are mirrored automatically by mirror-template-images.yml - # on push to develop, which backfills any unmirrored tag when the templates - # Dockerfile changes — so anyone pinning SUPABASE_INTERNAL_IMAGE_REGISTRY=ghcr.io - # doesn't inherit a `manifest unknown` failure. (No CI job pins that registry - # anymore -- see mirror-template-images.yml's own comment for why, CLI-1966.) - workflow_dispatch: -permissions: - contents: read - -jobs: - setup: - runs-on: blacksmith-2vcpu-ubuntu-2404 - outputs: - tags: ${{ steps.list.outputs.tags }} - curr: ${{ steps.curr.outputs.tags }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: apps/cli-go/go.mod - cache: true - - id: list - run: | - echo "tags=$(go run tools/listdep/main.go)" >> "$GITHUB_OUTPUT" - - id: curr - name: List main branch dependencies - if: github.ref != 'refs/heads/main' - run: | - git fetch origin main - git checkout main - echo "tags=$(go run tools/listdep/main.go)" >> "$GITHUB_OUTPUT" - - publish: - needs: - - setup - permissions: - contents: read - packages: write - id-token: write - if: ${{ needs.setup.outputs.tags != needs.setup.outputs.curr }} - strategy: - matrix: - src: ${{ fromJson(needs.setup.outputs.tags) }} - # Call workflow explicitly because events from actions cannot trigger more actions - uses: ./.github/workflows/cli-go-mirror-image.yml - with: - image: ${{ matrix.src }} - secrets: - PROD_AWS_ROLE: ${{ secrets.PROD_AWS_ROLE }} -defaults: - run: - working-directory: apps/cli-go diff --git a/.github/workflows/cli-go-tag-pkg.yml b/.github/workflows/cli-go-tag-pkg.yml deleted file mode 100644 index 3563c4077b..0000000000 --- a/.github/workflows/cli-go-tag-pkg.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Tag pkg - -on: - workflow_dispatch: - inputs: - version: - description: "pkg version to tag (e.g. v1.2.2)" - required: true - type: string - -permissions: {} - -jobs: - tag: - name: Create pkg tag - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: develop - fetch-depth: 0 - persist-credentials: false - - - name: Create and push pkg tag - run: | - VERSION="${INPUTS_VERSION}" - if ! [[ "$VERSION" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then - echo "Error: version '$VERSION' does not match semver format (e.g. v1.2.2)" - exit 1 - fi - TAG="pkg/$VERSION" - if git rev-parse "$TAG" >/dev/null 2>&1; then - echo "Error: tag '$TAG' already exists" - exit 1 - fi - git tag "$TAG" - git push origin "$TAG" - env: - INPUTS_VERSION: ${{ inputs.version }} -defaults: - run: - working-directory: apps/cli-go diff --git a/.github/workflows/cli-go-codeql.yml b/.github/workflows/codeql.yml similarity index 55% rename from .github/workflows/cli-go-codeql.yml rename to .github/workflows/codeql.yml index 3f19507312..a25cab48b7 100644 --- a/.github/workflows/cli-go-codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,21 +2,19 @@ name: "CodeQL" on: pull_request: - paths: - - apps/cli-go/** - merge_group: push: branches: - develop - paths: - - apps/cli-go/** + +concurrency: + group: codeql-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: analyze: name: Analyze (${{ matrix.language }}) - # Swift requires a macOS runner; every other language in the matrix uses blacksmith. - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'blacksmith-8vcpu-ubuntu-2404' }} - timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + runs-on: blacksmith-8vcpu-ubuntu-2404 + timeout-minutes: 360 permissions: # required for all workflows security-events: write @@ -32,8 +30,6 @@ jobs: fail-fast: false matrix: include: - - language: go - build-mode: autobuild - language: javascript-typescript build-mode: none steps: @@ -48,21 +44,7 @@ jobs: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - # Placeholder manual-build step; only runs if a language's build-mode above is set to "manual". - - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 - - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: category: "/language:${{matrix.language}}" -defaults: - run: - working-directory: apps/cli-go diff --git a/.github/workflows/fill-sticky-disks.yml b/.github/workflows/fill-sticky-disks.yml index 0e9da129b8..cfa187f3f7 100644 --- a/.github/workflows/fill-sticky-disks.yml +++ b/.github/workflows/fill-sticky-disks.yml @@ -1,6 +1,6 @@ name: Fill sticky disks -# Test jobs mount the pnpm, Go and Docker image caches from Blacksmith sticky +# Test jobs mount the pnpm and Docker image caches from Blacksmith sticky # disks but pull request runs never commit them. This workflow is the trusted # writer: it runs on protected branches, which are the only refs Blacksmith's # sticky-disk branch protection lets commit. @@ -56,7 +56,6 @@ jobs: # Starting a default project pulls every image `supabase start` uses. - name: Pull CLI images env: - SUPABASE_GO_BINARY: ${{ github.workspace }}/apps/cli-go/supabase-go SUPABASE_CLI_BINARY_OVERRIDE: ${{ github.workspace }}/apps/cli/dist/supabase run: | project="$(mktemp -d)" diff --git a/.github/workflows/flaky-check.yml b/.github/workflows/flaky-check.yml index 54c9bdfbfc..7b8ae01850 100644 --- a/.github/workflows/flaky-check.yml +++ b/.github/workflows/flaky-check.yml @@ -139,7 +139,6 @@ jobs: with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} sticky-disks: "true" - go-cache: ${{ matrix.suite == 'e2e' }} docker-images: ${{ matrix.suite != 'unit' && (matrix.suite != 'stack-e2e' || matrix.runtime == 'docker' || matrix.scenario == 'lifecycle' && matrix.runtime == 'native') }} native-artifacts: ${{ matrix.runtime == 'native' }} diff --git a/.github/workflows/github-scripts-ci.yml b/.github/workflows/github-scripts-ci.yml index 37a5c6cf43..55484ff381 100644 --- a/.github/workflows/github-scripts-ci.yml +++ b/.github/workflows/github-scripts-ci.yml @@ -34,7 +34,7 @@ jobs: test: name: Test and type-check runs-on: ubuntu-latest - # The shared setup installs the full workspace + Go toolchain via mise, which + # The shared setup installs the full workspace toolchain via mise, which # runs ~9-10 min; a 10-minute cap raced the install and got cancelled on a # cold cache. 20 gives that install headroom. (This check is heavier than it # needs to be for two scripts — slimming the setup is a possible follow-up.) diff --git a/.github/workflows/lint-pull-request.yml b/.github/workflows/lint-pull-request.yml index e69d6bd814..e93acdf712 100644 --- a/.github/workflows/lint-pull-request.yml +++ b/.github/workflows/lint-pull-request.yml @@ -52,7 +52,6 @@ jobs: api cli cli-e2e - cli-go cli-test-helpers config docs diff --git a/.github/workflows/cli-go-mirror-image.yml b/.github/workflows/mirror-image.yml similarity index 100% rename from .github/workflows/cli-go-mirror-image.yml rename to .github/workflows/mirror-image.yml diff --git a/.github/workflows/mirror-template-images.yml b/.github/workflows/mirror-template-images.yml index 0e63be1f2a..06b044abae 100644 --- a/.github/workflows/mirror-template-images.yml +++ b/.github/workflows/mirror-template-images.yml @@ -6,7 +6,7 @@ name: Mirror template images # Dockerfile changes on develop — most often via a merged catalog bump or # hotfix PR from slim-release-published.yml, or a Dependabot `docker` bump for # an upstream-only image — this workflow detects any tag that is not yet -# mirrored and backfills it the same way `cli-go-mirror-image.yml` does. +# mirrored and backfills it the same way `mirror-image.yml` does. # # It runs on `push` to develop (not on the PR) on purpose: mirroring needs the # AWS role + packages:write, which a dependabot-triggered `pull_request` run @@ -14,10 +14,9 @@ name: Mirror template images # backfill runs as soon as the bump lands on develop, repopulating ghcr.io/ECR # so anyone pinning SUPABASE_INTERNAL_IMAGE_REGISTRY=ghcr.io doesn't inherit a # `manifest unknown` failure. (No CI job pins that registry anymore -- the -# `cli-go-ci.yml` `Start` job that used to made it CI-observable was removed in -# CLI-1966 along with `internal/start`; this mirror now only serves users who -# opt into the mirror directly, and a mirroring failure no longer turns any -# check red.) +# `Start` job that used to make it CI-observable was removed in CLI-1966 along +# with `internal/start`; this mirror now only serves users who opt into the +# mirror directly, and a mirroring failure no longer turns any check red.) on: push: @@ -95,7 +94,7 @@ jobs: matrix: image: ${{ fromJson(needs.detect.outputs.missing) }} # Reuse the existing mirror logic (docker.io -> public.ecr.aws + ghcr.io). - uses: ./.github/workflows/cli-go-mirror-image.yml + uses: ./.github/workflows/mirror-image.yml with: image: ${{ matrix.image }} secrets: diff --git a/.github/workflows/cli-go-publish-migra.yml b/.github/workflows/publish-migra.yml similarity index 96% rename from .github/workflows/cli-go-publish-migra.yml rename to .github/workflows/publish-migra.yml index 7e37210911..ac061f0d3c 100644 --- a/.github/workflows/cli-go-publish-migra.yml +++ b/.github/workflows/publish-migra.yml @@ -86,11 +86,8 @@ jobs: packages: write id-token: write # Call workflow explicitly because events from actions cannot trigger more actions - uses: ./.github/workflows/cli-go-mirror-image.yml + uses: ./.github/workflows/mirror-image.yml with: image: ${{ needs.settings.outputs.image_tag }} secrets: PROD_AWS_ROLE: ${{ secrets.PROD_AWS_ROLE }} -defaults: - run: - working-directory: apps/cli-go diff --git a/.github/workflows/cli-go-pg-prove.yml b/.github/workflows/publish-pg-prove.yml similarity index 96% rename from .github/workflows/cli-go-pg-prove.yml rename to .github/workflows/publish-pg-prove.yml index 3c836bbdce..f2b1ad1c07 100644 --- a/.github/workflows/cli-go-pg-prove.yml +++ b/.github/workflows/publish-pg-prove.yml @@ -86,11 +86,8 @@ jobs: packages: write id-token: write # Call workflow explicitly because events from actions cannot trigger more actions - uses: ./.github/workflows/cli-go-mirror-image.yml + uses: ./.github/workflows/mirror-image.yml with: image: ${{ needs.settings.outputs.image_tag }} secrets: PROD_AWS_ROLE: ${{ secrets.PROD_AWS_ROLE }} -defaults: - run: - working-directory: apps/cli-go diff --git a/.github/workflows/slim-release-published.yml b/.github/workflows/slim-release-published.yml index 35317c0735..d54acfb5d3 100644 --- a/.github/workflows/slim-release-published.yml +++ b/.github/workflows/slim-release-published.yml @@ -151,10 +151,9 @@ jobs: generated_files=( packages/stack/src/Artifacts.ts apps/cli/src/shared/services/Dockerfile - apps/cli-go/pkg/config/templates/Dockerfile ) if git diff --quiet -- "${generated_files[@]}"; then - echo "${branch}: catalog and Dockerfiles already match ${release}; skipping." + echo "${branch}: catalog and Dockerfile already match ${release}; skipping." continue fi @@ -164,10 +163,10 @@ jobs: if ! push_output="$(git push --force "$push_url" "HEAD:refs/heads/${branch}" 2>&1)"; then echo "$push_output" if ! grep -q "queued for merging cannot be updated" <<<"$push_output"; then - echo "::error ::Failed to push ${branch}. Run manually: ${manual_prefix} ${release}, regenerate the Dockerfiles with \`bun apps/cli/scripts/render-service-dockerfile.ts\`, then open a pull request by hand." + echo "::error ::Failed to push ${branch}. Run manually: ${manual_prefix} ${release}, regenerate the Dockerfile with \`bun apps/cli/scripts/render-service-dockerfile.ts\`, then open a pull request by hand." exit 1 fi - manual_steps="run manually: ${manual_prefix} ${release}, regenerate the Dockerfiles with \`bun apps/cli/scripts/render-service-dockerfile.ts\`, then open a pull request by hand." + manual_steps="run manually: ${manual_prefix} ${release}, regenerate the Dockerfile with \`bun apps/cli/scripts/render-service-dockerfile.ts\`, then open a pull request by hand." queued_steps="Once ${branch} leaves the merge queue, ${manual_steps}" # The app token minted just before this step expires after one hour; capping the wait at # 50 minutes into the step leaves room for the lookups and the dispatch that follow it. diff --git a/.github/workflows/stack-stress.yml b/.github/workflows/stack-stress.yml index 2d10715a3f..e204ca5204 100644 --- a/.github/workflows/stack-stress.yml +++ b/.github/workflows/stack-stress.yml @@ -37,7 +37,6 @@ jobs: uses: ./.github/actions/setup with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} - go-cache: "false" - name: Setup Podman if: matrix.runtime == 'podman' diff --git a/.github/workflows/sync-repair.yml b/.github/workflows/sync-repair.yml index dea2a6ec30..d86840984b 100644 --- a/.github/workflows/sync-repair.yml +++ b/.github/workflows/sync-repair.yml @@ -8,7 +8,6 @@ on: # jactionlint ignore=dangerous-triggers workflow_run: workflows: - - CI - Test - AI Review - GitHub Scripts CI diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f2fe2ca454..fbf89feabb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -73,9 +73,6 @@ jobs: with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} sticky-disks: "true" - - name: Unlock keyring (for cli-go keyring tests) - uses: t1m0thyj/unlock-keyring@cbcf205c879ebd86add70bab3a6abfcce59a5cae # v1.2.0 - - name: Check code quality run: pnpm run check:all @@ -110,7 +107,6 @@ jobs: with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} sticky-disks: "true" - go-cache: "false" - name: Run unit tests run: pnpm run test:unit @@ -145,7 +141,6 @@ jobs: with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} sticky-disks: "true" - go-cache: "false" docker-images: "true" - name: Run integration tests @@ -242,7 +237,6 @@ jobs: uses: ./.github/actions/setup with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} - go-cache: "false" tools: node bun pnpm # Windows unpacks the store archive slower than it downloads the packages. pnpm-store-cache: ${{ matrix.os != 'windows-latest' }} @@ -316,7 +310,6 @@ jobs: uses: ./.github/actions/setup with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} - go-cache: "false" # Keyed per suite so each job saves the artifacts it actually prepared. - name: Cache native stack artifacts @@ -407,14 +400,11 @@ jobs: if: matrix.suite == 'cli' run: pnpm exec turbo run test:e2e:run --only --filter=supabase -- --shard=${{ matrix.shard }}/${{ matrix.shards }} env: - SUPABASE_GO_BINARY: ${{ github.workspace }}/apps/cli-go/supabase-go SUPABASE_E2E_WORKERS: "4" - name: Run cli-e2e end-to-end tests if: matrix.suite == 'cli-e2e' run: pnpm exec turbo run test:e2e:run --only --filter=@supabase/cli-e2e -- --shard=${{ matrix.shard }}/${{ matrix.shards }} - env: - SUPABASE_GO_BINARY: ${{ github.workspace }}/apps/cli-go/supabase-go # Vitest writes its results cache under a sha1-named directory. The refresh script # (apps/cli/scripts/refresh-e2e-timings.ts) merges these into tests/e2e-timings.json. @@ -459,7 +449,6 @@ jobs: with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} sticky-disks: "true" - go-cache: "false" docker-images: ${{ matrix.runtime == 'docker' }} native-artifacts: ${{ matrix.runtime == 'native' }} @@ -507,7 +496,6 @@ jobs: with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} sticky-disks: "true" - go-cache: "false" - name: Setup Podman uses: ./.github/actions/setup-podman diff --git a/.github/workflows/verify-install-channels.yml b/.github/workflows/verify-install-channels.yml index 66402ba70e..82cba9a036 100644 --- a/.github/workflows/verify-install-channels.yml +++ b/.github/workflows/verify-install-channels.yml @@ -6,9 +6,7 @@ name: Verify Install Channels # an already-published version. # # Performs a real install so a manifest/tarball checksum mismatch (as in CLI v2.107.0) reproduces -# here instead of being trusted from the publish step. Each leg also directly checks that the -# `supabase-go` sidecar binary is present and executable in the install directory, checking the -# file rather than a CLI command, since a command could later be natively ported off the sidecar. +# here instead of being trusted from the publish step. on: workflow_call: @@ -137,23 +135,8 @@ jobs: echo "Version mismatch: expected ${expected}, got ${actual}" >&2 exit 1 fi - - name: Verify Go sidecar - run: | - set -euo pipefail - # Homebrew's `bin.install` symlinks both `supabase` and `supabase-go` - # into the same prefix bin/ directory, so the sidecar must sit right - # next to whichever `supabase` resolved from PATH. - bin_dir="$(dirname "$(command -v supabase)")" - sidecar="${bin_dir}/supabase-go" - if [ ! -e "${sidecar}" ]; then - echo "Go sidecar probe failed: ${sidecar} does not exist" >&2 - exit 1 - fi - if [ ! -x "${sidecar}" ]; then - echo "Go sidecar probe failed: ${sidecar} exists but is not executable" >&2 - exit 1 - fi - echo "Go sidecar probe OK: ${sidecar}" + - name: Exercise supabase --help + run: supabase --help scoop: name: Scoop (${{ inputs.scoop_name }}) @@ -191,23 +174,9 @@ jobs: echo "Version mismatch: expected ${expected}, got ${actual}" >&2 exit 1 fi - - name: Verify Go sidecar + - name: Exercise supabase --help shell: bash - run: | - set -euo pipefail - # Scoop's manifest only declares `supabase.exe` in `bin` (see - # apps/cli/scripts/update-scoop.ts), so only `supabase` gets a shim in - # ~/scoop/shims — `dirname "$(command -v supabase)"` would resolve to - # the shim directory, not the real install directory supabase-go.exe - # actually lives in. Go straight to the app's current version - # directory instead, which Scoop always maintains regardless of shims. - app_dir="${HOME}/scoop/apps/${SCOOP_NAME}/current" - sidecar="${app_dir}/supabase-go.exe" - if [ ! -e "${sidecar}" ]; then - echo "Go sidecar probe failed: ${sidecar} does not exist" >&2 - exit 1 - fi - echo "Go sidecar probe OK: ${sidecar}" + run: supabase --help install-script: name: install script (${{ matrix.runner }}) @@ -251,21 +220,6 @@ jobs: echo "Version mismatch: expected ${expected}, got ${actual}" >&2 exit 1 fi - - name: Verify Go sidecar + - name: Exercise supabase --help shell: bash - run: | - set -euo pipefail - # The install script places `supabase-go` right next to `supabase`, - # so the sidecar must sit in the same directory `supabase` resolved - # from on PATH. - bin_dir="$(dirname "$(command -v supabase)")" - sidecar="${bin_dir}/supabase-go" - if [ ! -e "${sidecar}" ]; then - echo "Go sidecar probe failed: ${sidecar} does not exist" >&2 - exit 1 - fi - if [ ! -x "${sidecar}" ]; then - echo "Go sidecar probe failed: ${sidecar} exists but is not executable" >&2 - exit 1 - fi - echo "Go sidecar probe OK: ${sidecar}" + run: supabase --help diff --git a/.oxfmtrc.json b/.oxfmtrc.json index e364381a7e..33527f22c6 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -3,7 +3,6 @@ ".repos/", "apps/cli/src/shared/feedback/database.types.ts", "packages/stack/src/functions/generated/", - "apps/cli-go/", "apps/cli-e2e/fixtures/", "apps/docs/content/docs/commands/", "apps/docs/public/", diff --git a/.oxlintrc.effect.json b/.oxlintrc.effect.json index ee9c33a8e9..9712e56950 100644 --- a/.oxlintrc.effect.json +++ b/.oxlintrc.effect.json @@ -20,6 +20,13 @@ "apps/cli/src/shared/feedback/database.types.ts", "apps/cli/src/command-internal/db-bootstrap/local-db-running.integration.test.ts", "apps/cli/src/command-internal/db-bootstrap/local-db-running.ts", + "apps/cli/src/command-internal/config-validate.parity.unit.test.ts", + "apps/cli/src/command-internal/hosted-consent/hosted-consent-coverage.unit.test.ts", + "apps/cli/tests/helpers/config-goldens.ts", + "apps/cli/tests/helpers/config-values-layer.integration.test.ts", + "apps/cli/tests/helpers/db-target-selection.ts", + "apps/cli/tests/helpers/hosted-consent.ts", + "apps/cli/tests/helpers/hosted-target-selection.ts", "packages/api/src/generated/contracts.ts" ] } diff --git a/.oxlintrc.json b/.oxlintrc.json index 9df557b0bb..51bff7da6a 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -6,7 +6,6 @@ }, "ignorePatterns": [ ".repos", - "apps/cli-go", // Generated by `supabase gen types` (see `gen:feedback-types` in apps/cli). "apps/cli/src/shared/feedback/database.types.ts", "apps/cli-e2e/fixtures", @@ -45,6 +44,40 @@ "supabase/no-raw-management-api-url": "error" } }, + { + // Ambient env reads go through `CliConfigValues` (registry names) or the shared env-option + // helpers, so a stray `process.env` can't bypass the flag > shell > .env > config order. + "files": ["apps/cli/src/**"], + "excludeFiles": [ + "apps/cli/src/**/*.test.ts", + "apps/cli/src/shared/config/cli-config-provider.layer.ts", + "apps/cli/src/shared/config/cli-config-env.ts", + // Standalone node shim built with `bun build --target node`; it cannot import effect. + "apps/cli/src/shared/cli/bin.ts", + // Templates that run inside the user's deployed container, not in the CLI process. + "apps/cli/src/shared/compute/stacks/**" + ], + "rules": { + "no-restricted-properties": [ + "error", + { + "object": "process", + "property": "env", + "message": "Read env through CliConfigValues or the shared env helpers, not process.env." + }, + { + "object": "Bun", + "property": "env", + "message": "Read env through CliConfigValues or the shared env helpers, not Bun.env." + }, + { + "object": "globalThis", + "property": "process", + "message": "Use the bare `process` global; reading env through globalThis.process bypasses CliConfigValues." + } + ] + } + }, { // `.github/scripts` is the only `bun:test` consumer; `@types/bun`'s shared `Matchers` // types `.rejects.toThrow(...)` as returning `void`, even though it must be awaited at @@ -76,6 +109,108 @@ ] } }, + { + // Env must not be imported past `CliConfigValues`: not `env` from node:process/bun, and + // `ambientEnvironment` only in the audited files of the next override. This rule replaces the + // repo-wide one for these files, so the namespace pattern is repeated; keep the two in sync. + "files": ["apps/cli/src/**"], + "excludeFiles": [ + "apps/cli/src/**/*.test.ts", + "apps/cli/src/shared/config/cli-config-provider.layer.ts", + "apps/cli/src/shared/config/cli-config-env.ts", + "apps/cli/src/shared/cli/bin.ts", + "apps/cli/src/shared/compute/stacks/**", + "apps/cli/src/cli/complete.ts", + "apps/cli/src/command-internal/colors.ts", + "apps/cli/src/command-internal/db-config.parse.ts", + "apps/cli/src/command-internal/hostname.ts", + "apps/cli/src/command-internal/pgpass.ts", + "apps/cli/src/commands/login/login-claude-hint.ts", + "apps/cli/src/commands/start/lib/env-or-default.ts", + "apps/cli/src/commands/start/services/vector.service.ts", + "apps/cli/src/shared/functions/deploy.ts" + ], + "rules": { + "no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "node:process", + "importNames": ["env"], + "message": "Read env through CliConfigValues or the shared env helpers, not node:process env." + }, + { + "name": "process", + "importNames": ["env"], + "message": "Read env through CliConfigValues or the shared env helpers, not process env." + }, + { + "name": "bun", + "importNames": ["env"], + "message": "Read env through CliConfigValues or the shared env helpers, not Bun's env export." + } + ], + "patterns": [ + { + "group": ["**/cli-config-provider.layer.ts"], + "importNames": ["ambientEnvironment"], + "message": "Read env through CliConfigValues; only the audited non-registry readers may import ambientEnvironment." + }, + { + "group": ["**/namespace/**"], + "message": "Namespace internals are private; consume them through StackNamespace.ts." + } + ] + } + ] + } + }, + { + // These read terminal, libpq, docker and proxy variables, never a config registry name, so + // they may import `ambientEnvironment`; the rest of the override above still applies. + "files": [ + "apps/cli/src/cli/complete.ts", + "apps/cli/src/command-internal/colors.ts", + "apps/cli/src/command-internal/db-config.parse.ts", + "apps/cli/src/command-internal/hostname.ts", + "apps/cli/src/command-internal/pgpass.ts", + "apps/cli/src/commands/login/login-claude-hint.ts", + "apps/cli/src/commands/start/lib/env-or-default.ts", + "apps/cli/src/commands/start/services/vector.service.ts", + "apps/cli/src/shared/functions/deploy.ts" + ], + "rules": { + "no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "node:process", + "importNames": ["env"], + "message": "Read env through CliConfigValues or the shared env helpers, not node:process env." + }, + { + "name": "process", + "importNames": ["env"], + "message": "Read env through CliConfigValues or the shared env helpers, not process env." + }, + { + "name": "bun", + "importNames": ["env"], + "message": "Read env through CliConfigValues or the shared env helpers, not Bun's env export." + } + ], + "patterns": [ + { + "group": ["**/namespace/**"], + "message": "Namespace internals are private; consume them through StackNamespace.ts." + } + ] + } + ] + } + }, { // Default for the whole stack package (`src/**` and `tests/**`): on top of the repo-wide // namespace restriction above, raw `node:fs`/`node:sqlite` are confined to namespace drivers diff --git a/AGENTS.md b/AGENTS.md index b6b56afec5..3afcece062 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -112,7 +112,7 @@ page, or upstream issue. If the rationale needs more than three lines, move it t ### Never write - Code narration, provenance/history, evidence trails, ticket IDs as provenance, or meta-commentary on code shape. A ticket ID belongs only in a `TODO(CLI-1234):` or when no ADR exists and the ticket is the only home for a decision. -- Restatements of docs, section banners, or Go-parity framing. Link to maintained docs instead. +- Restatements of docs, or section banners. Link to maintained docs instead. - ALL-CAPS emphasis or words such as “deliberately”, “crucially”, and “exactly”. Tests should carry intent in their names; comments only explain non-obvious fixture setup. Keep diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 20b02d8a8a..ae3a96014d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ Pull requests from external contributors that do not follow this workflow are co ### Tool versions -This repo pins the versions of Node, Bun, Go, pnpm, and golangci-lint that contributors are expected to build against, and uses [`mise`](https://mise.jdx.dev/) — a polyglot version manager — to install and activate them automatically. If you don't already have these tools installed, `mise` is a great way to get up and running quickly. +This repo pins the versions of Node, Bun, and pnpm that contributors are expected to build against, and uses [`mise`](https://mise.jdx.dev/) — a polyglot version manager — to install and activate them automatically. If you don't already have these tools installed, `mise` is a great way to get up and running quickly. #### Installing mise @@ -52,23 +52,19 @@ mise install `mise install` resolves the versions this repo expects from a handful of files, rather than hardcoding them all in one place: -| Tool | Version source | -| ------------- | --------------------------------------------------- | -| Bun | `.bun-version` | -| Node.js | `.node-version` | -| pnpm | `devEngines.packageManager` field in `package.json` | -| Go | `mise.toml` | -| golangci-lint | `mise.toml` | -| jactionlint | `mise.toml` | -| shellcheck | `mise.toml` | - -The Go and golangci-lint entries in `mise.toml` are intentionally temporary while the Go CLI remains in the repo. The canonical Go module metadata still lives in `apps/cli-go/go.mod`; keep the `mise.toml` entries aligned only until the Go code is removed. +| Tool | Version source | +| ----------- | --------------------------------------------------- | +| Bun | `.bun-version` | +| Node.js | `.node-version` | +| pnpm | `devEngines.packageManager` field in `package.json` | +| jactionlint | `mise.toml` | +| shellcheck | `mise.toml` | Once installed, `mise` activates these versions automatically whenever your shell is inside this repo — no manual `nvm use`, `gvm use`, or similar switching required. #### Without mise -`mise` is not required. If you already have Bun, Node, pnpm, Go, jactionlint, and shellcheck installed and managed some other way, just make sure your versions match the ones pinned in `.bun-version`, `.node-version`, `mise.toml`, `package.json`, and `apps/cli-go/go.mod`. `pnpm check:all` runs jactionlint, which only checks workflow `run:` scripts when shellcheck is on `PATH`. +`mise` is not required. If you already have Bun, Node, pnpm, jactionlint, and shellcheck installed and managed some other way, just make sure your versions match the ones pinned in `.bun-version`, `.node-version`, `mise.toml`, and `package.json`. `pnpm check:all` runs jactionlint, which only checks workflow `run:` scripts when shellcheck is on `PATH`. ### Install dependencies @@ -180,16 +176,14 @@ pnpm run test:unit && pnpm run test:integration ``` The root unit and integration scripts use Turbo to fan out the package-local -`test:*:run` tasks across the standard TypeScript/Vitest workspaces. The Go -workspace remains package-local because its tests run directly through Go: -`pnpm --dir apps/cli-go run test:unit`. Go tests are covered by the dedicated -Go CI workflow. Unit and integration tasks are uncached for now; e2e tasks are -also uncached and run one package at a time. Forward a Vitest shard to every -e2e package with `pnpm run test:e2e --shard=1/3`. +`test:*:run` tasks across the standard TypeScript/Vitest workspaces. Unit and +integration tasks are uncached for now; e2e tasks are also uncached and run +one package at a time. Forward a Vitest shard to every e2e package with +`pnpm run test:e2e --shard=1/3`. ## E2E Compatibility Test Suite -`apps/cli-e2e` implements the replay-and-record compatibility harness for the TypeScript CLI. Live tests are owned by `apps/cli` and run from the command they cover. The CLI still shells out to the bundled Go binary for the handful of commands the TS port proxies (`db diff --use-pg-schema`, `db branch *`, `db remote changes`, `gen keys`, `functions download`), so `apps/cli-go/` is built alongside the TS CLI for these suites, but there is no Go-vs-TypeScript parity runner. +`apps/cli-e2e` implements the replay-and-record compatibility harness for the TypeScript CLI. Live tests are owned by `apps/cli` and run from the command they cover. ### Architecture @@ -286,7 +280,6 @@ Test a real end-to-end publish and install of the CLI against a local npm regist ### Prerequisites - **Bun** — for compiling the CLI binary and running the scripts -- **Go** — required to build the `supabase-go` sidecar (commands proxied to the Go binary) - **pnpm** — already required by this repo - **Node.js** — required by `npx` / `npm install -g` to test the published package @@ -333,8 +326,6 @@ supabase --version | Problem | Fix | | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Error: Something is already running on port 4873` | Kill the leftover Verdaccio process (`lsof -ti:4873 \| xargs kill`) and retry | -| `go not found in PATH` | Install Go from https://go.dev/dl/ | -| `Error: Go CLI source not found` | Run `pnpm repos:install` to clone `apps/cli-go` | | `npm` / `pnpm` tries to fetch from `localhost:4873` when no registry is running | Stale global registry override left behind by an older version of `local-registry.ts` (the current script never modifies global config). Run `npm config delete registry` and `pnpm config delete registry`. Note that pnpm stores the override in its own global config (`~/Library/Preferences/pnpm/auth.ini` on macOS, `~/.config/pnpm/` on Linux), not `~/.npmrc` — check there if the delete command fails | | `npx` resolves from npm instead of local | Pass `--registry http://localhost:4873` explicitly to `npx` / `npm install` | @@ -363,15 +354,15 @@ pnpm run build pnpm run generate ``` -**Build only the CLI and its Go sidecar:** +**Build only the CLI:** ```sh pnpm exec turbo run supabase#build ``` -The CLI build names its config and Go build prerequisites explicitly. The -remaining CLI workspace dependencies intentionally have no build script, so -strict Turbo task selection does not synthesize no-op `^build` tasks for them. +The CLI build names its config build prerequisite explicitly. The remaining +CLI workspace dependencies intentionally have no build script, so strict +Turbo task selection does not synthesize no-op `^build` tasks for them. **Run the live suite:** diff --git a/apps/cli-go/LICENSE b/LICENSE similarity index 100% rename from apps/cli-go/LICENSE rename to LICENSE diff --git a/README.md b/README.md index cebc9eaab6..f9515f796d 100644 --- a/README.md +++ b/README.md @@ -77,12 +77,14 @@ supabase link ## Manage Your Database -Create migrations, compare schemas, and apply changes locally or to your linked project: +Create migrations, compare schemas, and apply changes to your local database. Add `--remote` to act +on your linked project instead: ```sh supabase migration new create_profiles supabase db diff supabase db push +supabase db push --remote supabase db reset ``` @@ -101,8 +103,8 @@ supabase functions deploy hello-world Generate TypeScript types from your local database or linked project: ```sh -supabase gen types --local -supabase gen types --linked +supabase gen types +supabase gen types --remote ``` ## Reference @@ -137,7 +139,6 @@ Useful source entry points: | Path | Purpose | | ----------------- | ------------------------------------ | | `apps/cli` | TypeScript/Bun CLI package | -| `apps/cli-go` | Go CLI source used by the CLI | | `packages/stack` | Local Supabase stack runtime | | `packages/config` | Config schema and generated types | | `packages/api` | Typed Supabase Management API client | diff --git a/apps/cli-e2e/.env.example b/apps/cli-e2e/.env.example index d437835f33..fb502b3f75 100644 --- a/apps/cli-e2e/.env.example +++ b/apps/cli-e2e/.env.example @@ -7,12 +7,6 @@ CLI_E2E_MODE=record # Staging Management API token. Required in record mode. SUPABASE_ACCESS_TOKEN=sbp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -# The CLI shells out to the bundled Go binary for the proxied commands -# (db diff --use-pg-schema, db branch *, db remote changes, gen keys, -# functions download --legacy-bundle). Point at a freshly built binary: -# cd apps/cli-go && go build -o /tmp/supabase-test-binary . -SUPABASE_GO_BINARY=/tmp/supabase-test-binary - # --- Optional record overrides (sensible defaults in src/tests/env.ts) --- # Management API base (also accepted as SUPABASE_STAGING_URL). # CLI_E2E_API_URL=https://api.supabase.green diff --git a/apps/cli-e2e/AGENTS.md b/apps/cli-e2e/AGENTS.md index 41f46855f0..7cd57a5f91 100644 --- a/apps/cli-e2e/AGENTS.md +++ b/apps/cli-e2e/AGENTS.md @@ -64,7 +64,7 @@ the running tests exercise. The implications: import { testBehaviour } from "./test-context.ts"; testBehaviour("does the thing", async ({ run, projectRef, orgId, apiUrl, workspace }) => { - const result = await run(["command", "--project-ref", projectRef]); + const result = await run(["command", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Expected output"); }); @@ -91,7 +91,7 @@ testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["command", "--project-ref", PROJECT_REF]); + const result = await run(["command", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); ``` @@ -180,32 +180,3 @@ per-process, so each shard still has deterministic intra-shard ordering. is a single-job operation; parallel shards would race on the shared `fixtures/recorded/` directory. The `record` script does not accept `--shard`. - -## Go binary version requirement - -The CLI proxies a fixed, small set of commands to a Go binary (`SUPABASE_GO_BINARY` → bundled package binary → system `supabase`) — as of CLI-1970, `apps/cli-go/` contains only that residual proxied subset, nothing else, and it is slated for cleanup and removal (the surface only shrinks; never add tests that grow it). If your system `supabase` binary predates a flag or subcommand change on one of these, `testBehaviour` tests for it will fail with "unknown command" or "unknown flag". - -Build the Go CLI from source and point `SUPABASE_GO_BINARY` at it: - -```sh -(cd apps/cli-go && go build -o /tmp/supabase-test-binary .) - -# Replay -SUPABASE_GO_BINARY=/tmp/supabase-test-binary \ - pnpm exec turbo run @supabase/cli-e2e#test:e2e:run - -# Record -SUPABASE_GO_BINARY=/tmp/supabase-test-binary \ - SUPABASE_ACCESS_TOKEN=sbp_... SUPABASE_STAGING_URL=https://api.supabase.green \ - pnpm run record -``` - -`SUPABASE_GO_BINARY` is inherited by the CLI subprocess via `exec()` in the harness, so you only need to set it once in the shell. - -Commands currently requiring this — the full proxied surface, nothing else needs a Go binary at all: - -- `db diff` (for `--use-pg-schema`) -- `db branch create`, `db branch delete`, `db branch list`, `db branch switch` -- `db remote changes` -- `gen keys` -- `functions download` (for the hidden `--legacy-bundle` flag) diff --git a/apps/cli-e2e/fixtures/scenarios/db-dump-exits-non-zero-on-connection-refused-with-local/interactions.json b/apps/cli-e2e/fixtures/scenarios/db-dump-exits-non-zero-on-connection-refused-without-remote/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/db-dump-exits-non-zero-on-connection-refused-with-local/interactions.json rename to apps/cli-e2e/fixtures/scenarios/db-dump-exits-non-zero-on-connection-refused-without-remote/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/db-lint-exits-non-zero-on-connection-refused-with-local/interactions.json b/apps/cli-e2e/fixtures/scenarios/db-lint-exits-non-zero-on-connection-refused-without-remote/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/db-lint-exits-non-zero-on-connection-refused-with-local/interactions.json rename to apps/cli-e2e/fixtures/scenarios/db-lint-exits-non-zero-on-connection-refused-without-remote/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/db-pull-exits-non-zero-on-connection-refused-with-local/interactions.json b/apps/cli-e2e/fixtures/scenarios/db-pull-exits-non-zero-on-connection-refused-without-remote/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/db-pull-exits-non-zero-on-connection-refused-with-local/interactions.json rename to apps/cli-e2e/fixtures/scenarios/db-pull-exits-non-zero-on-connection-refused-without-remote/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/db-push-local-exits-non-zero-on-connection-refused-with-local/interactions.json b/apps/cli-e2e/fixtures/scenarios/db-push-local-exits-non-zero-on-connection-refused-without-remote/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/db-push-local-exits-non-zero-on-connection-refused-with-local/interactions.json rename to apps/cli-e2e/fixtures/scenarios/db-push-local-exits-non-zero-on-connection-refused-without-remote/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/db-push-linked-exits-non-zero-on-connection-refused-with-linked/interactions.json b/apps/cli-e2e/fixtures/scenarios/db-push-remote-exits-non-zero-on-connection-refused-with-remote/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/db-push-linked-exits-non-zero-on-connection-refused-with-linked/interactions.json rename to apps/cli-e2e/fixtures/scenarios/db-push-remote-exits-non-zero-on-connection-refused-with-remote/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/db-query-linked-exits-non-zero-on-401/interactions.json b/apps/cli-e2e/fixtures/scenarios/db-query-remote-exits-non-zero-on-401/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/db-query-linked-exits-non-zero-on-401/interactions.json rename to apps/cli-e2e/fixtures/scenarios/db-query-remote-exits-non-zero-on-401/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/db-query-linked-exits-non-zero-on-403/interactions.json b/apps/cli-e2e/fixtures/scenarios/db-query-remote-exits-non-zero-on-403/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/db-query-linked-exits-non-zero-on-403/interactions.json rename to apps/cli-e2e/fixtures/scenarios/db-query-remote-exits-non-zero-on-403/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/db-query-linked-returns-json-with-output-json/interactions.json b/apps/cli-e2e/fixtures/scenarios/db-query-remote-returns-json-with-output-json/interactions.json similarity index 97% rename from apps/cli-e2e/fixtures/scenarios/db-query-linked-returns-json-with-output-json/interactions.json rename to apps/cli-e2e/fixtures/scenarios/db-query-remote-returns-json-with-output-json/interactions.json index 4ce1bb15f0..820418ae8a 100644 --- a/apps/cli-e2e/fixtures/scenarios/db-query-linked-returns-json-with-output-json/interactions.json +++ b/apps/cli-e2e/fixtures/scenarios/db-query-remote-returns-json-with-output-json/interactions.json @@ -13,7 +13,8 @@ "user-agent": "SupabaseCLI/" }, "body": { - "query": "SELECT 1" + "query": "SELECT 1", + "read_only": true } }, "response": { diff --git a/apps/cli-e2e/fixtures/scenarios/db-query-linked-returns-select-1-result-in-table-format/interactions.json b/apps/cli-e2e/fixtures/scenarios/db-query-remote-returns-select-1-result-in-table-format/interactions.json similarity index 97% rename from apps/cli-e2e/fixtures/scenarios/db-query-linked-returns-select-1-result-in-table-format/interactions.json rename to apps/cli-e2e/fixtures/scenarios/db-query-remote-returns-select-1-result-in-table-format/interactions.json index 533edf3271..37938ca646 100644 --- a/apps/cli-e2e/fixtures/scenarios/db-query-linked-returns-select-1-result-in-table-format/interactions.json +++ b/apps/cli-e2e/fixtures/scenarios/db-query-remote-returns-select-1-result-in-table-format/interactions.json @@ -13,7 +13,8 @@ "user-agent": "SupabaseCLI/" }, "body": { - "query": "SELECT 1" + "query": "SELECT 1", + "read_only": true } }, "response": { diff --git a/apps/cli-e2e/fixtures/scenarios/db-reset-exits-non-zero-on-connection-refused-with-local/interactions.json b/apps/cli-e2e/fixtures/scenarios/db-reset-exits-non-zero-on-connection-refused-without-remote/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/db-reset-exits-non-zero-on-connection-refused-with-local/interactions.json rename to apps/cli-e2e/fixtures/scenarios/db-reset-exits-non-zero-on-connection-refused-without-remote/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/gen-types-exits-non-zero-with-no-data-source-specified/interactions.json b/apps/cli-e2e/fixtures/scenarios/gen-types-exits-non-zero-when-the-local-stack-is-not-running/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/gen-types-exits-non-zero-with-no-data-source-specified/interactions.json rename to apps/cli-e2e/fixtures/scenarios/gen-types-exits-non-zero-when-the-local-stack-is-not-running/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/inspect-flags-rejects-db-url-with-local/interactions.json b/apps/cli-e2e/fixtures/scenarios/inspect-flags-rejects-remote-with-local/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/inspect-flags-rejects-db-url-with-local/interactions.json rename to apps/cli-e2e/fixtures/scenarios/inspect-flags-rejects-remote-with-local/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/remote-target-selection-reads-a-space-separated-remote-ref-as-remote-ref/interactions.json b/apps/cli-e2e/fixtures/scenarios/remote-target-selection-reads-a-space-separated-remote-ref-as-remote-ref/interactions.json new file mode 100644 index 0000000000..0790d147ea --- /dev/null +++ b/apps/cli-e2e/fixtures/scenarios/remote-target-selection-reads-a-space-separated-remote-ref-as-remote-ref/interactions.json @@ -0,0 +1,69 @@ +[ + { + "request": { + "method": "GET", + "path": "/v1/projects/replayfixtureprojref/advisors/security", + "query": {}, + "headers": { + "accept-encoding": "gzip", + "authorization": "Bearer __ACCESS_TOKEN__", + "host": "localhost:__PORT__", + "user-agent": "SupabaseCLI/" + }, + "body": null + }, + "response": { + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "x-gotrue-id": "__UUID__", + "x-ratelimit-limit": "120", + "x-ratelimit-remaining": "119", + "x-ratelimit-reset": "60" + }, + "body": { + "lints": [] + } + } + }, + { + "request": { + "method": "GET", + "path": "/v1/projects/replayfixtureprojref", + "query": {}, + "headers": { + "accept-encoding": "gzip", + "authorization": "Bearer __ACCESS_TOKEN__", + "host": "localhost:__PORT__", + "user-agent": "SupabaseCLI/" + }, + "body": null + }, + "response": { + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "x-gotrue-id": "__UUID__", + "x-ratelimit-limit": "120", + "x-ratelimit-remaining": "116", + "x-ratelimit-reset": "54" + }, + "body": { + "id": "replayfixtureprojref", + "ref": "replayfixtureprojref", + "organization_id": "replayfixtureprojref", + "organization_slug": "replayfixtureprojref", + "name": "cli-e2e-test", + "region": "us-east-1", + "status": "ACTIVE_HEALTHY", + "database": { + "host": "db.replayfixtureprojref.supabase.red", + "version": "17.6.1.111", + "postgres_engine": "17", + "release_channel": "ga" + }, + "created_at": "2000-01-01T00:00:00Z" + } + } + } +] diff --git a/apps/cli-e2e/fixtures/scenarios/test-db-exits-non-zero-on-connection-refused-with-local/interactions.json b/apps/cli-e2e/fixtures/scenarios/remote-target-selection-rejects-remote-url-on-a-hosted-command/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/test-db-exits-non-zero-on-connection-refused-with-local/interactions.json rename to apps/cli-e2e/fixtures/scenarios/remote-target-selection-rejects-remote-url-on-a-hosted-command/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/test-db-exits-non-zero-on-connection-refused-without-remote/interactions.json b/apps/cli-e2e/fixtures/scenarios/test-db-exits-non-zero-on-connection-refused-without-remote/interactions.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/apps/cli-e2e/fixtures/scenarios/test-db-exits-non-zero-on-connection-refused-without-remote/interactions.json @@ -0,0 +1 @@ +[] diff --git a/apps/cli-e2e/src/server/pg-mock.ts b/apps/cli-e2e/src/server/pg-mock.ts index b419eed1b1..f187f3cfed 100644 --- a/apps/cli-e2e/src/server/pg-mock.ts +++ b/apps/cli-e2e/src/server/pg-mock.ts @@ -367,7 +367,7 @@ function processMessages(socket: Bun.Socket, getState: () => PgMockS socket.data.buf = buf.slice(totalLen); if (msgType === 0x51) { - // 'Q' — simple query (used by pgconn.CopyTo and simple-protocol callers) + // 'Q' — simple query (used by COPY and simple-protocol callers) const query = payload.slice(0, payload.length - 1).toString("utf8"); const isProbe = /^\s*select\s+1\s*;?\s*$/i.test(query); const isCopy = /^\s*COPY\s+/i.test(query); diff --git a/apps/cli-e2e/src/server/request-matcher.ts b/apps/cli-e2e/src/server/request-matcher.ts index b77e02e0fd..7f75ca9fd5 100644 --- a/apps/cli-e2e/src/server/request-matcher.ts +++ b/apps/cli-e2e/src/server/request-matcher.ts @@ -86,7 +86,7 @@ export function resetCounters(counters: SequenceCounters): void { /** Sorts array bodies canonically so comparison is order-insensitive where * element order isn't semantically meaningful (e.g. bulk-create secrets) — a - * proxied command building a request body from a Go map can produce a + * proxied command building a request body from a map can produce a * different key order per platform. Nested object fields are still compared * exactly; only the top-level array order is normalized. */ export function sortBody(body: unknown): unknown { diff --git a/apps/cli-e2e/src/tests/advanced-platform-features.e2e.test.ts b/apps/cli-e2e/src/tests/advanced-platform-features.e2e.test.ts index 052284a2f0..b3a9e76493 100644 --- a/apps/cli-e2e/src/tests/advanced-platform-features.e2e.test.ts +++ b/apps/cli-e2e/src/tests/advanced-platform-features.e2e.test.ts @@ -9,8 +9,7 @@ describe("postgres-config", () => { "postgres-config", "get", "--experimental", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).not.toBe(""); @@ -25,8 +24,7 @@ describe("postgres-config", () => { "--experimental", "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout); @@ -41,8 +39,7 @@ describe("postgres-config", () => { "get", "--experimental", "--debug", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); @@ -58,8 +55,7 @@ describe("postgres-config", () => { "postgres-config", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -75,8 +71,7 @@ describe("postgres-config", () => { "postgres-config", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -92,8 +87,7 @@ describe("postgres-config", () => { "postgres-config", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -109,8 +103,7 @@ describe("postgres-config", () => { "postgres-config", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -126,8 +119,7 @@ describe("postgres-config", () => { "postgres-config", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -142,8 +134,8 @@ describe("postgres-config", () => { "--experimental", "--config", "max_connections=200", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -157,8 +149,8 @@ describe("postgres-config", () => { "max_connections=200", "--config", "shared_buffers=256MB", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -173,8 +165,8 @@ describe("postgres-config", () => { "--config", "max_connections=200", "--replace-existing-overrides", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }, @@ -190,8 +182,8 @@ describe("postgres-config", () => { "--config", "max_connections=200", "--no-restart", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }, @@ -209,8 +201,8 @@ describe("postgres-config", () => { "--experimental", "--config", "max_connections=200", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -228,8 +220,8 @@ describe("postgres-config", () => { "--experimental", "--config", "max_connections=200", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -250,8 +242,8 @@ describe("postgres-config", () => { "--experimental", "--config", "invalid_key=value", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("unrecognized config key"); @@ -269,8 +261,8 @@ describe("postgres-config", () => { "--experimental", "--config", "max_connections=200", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -288,8 +280,8 @@ describe("postgres-config", () => { "--experimental", "--config", "max_connections=200", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -304,8 +296,8 @@ describe("postgres-config", () => { "--experimental", "--config", "max_connections", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -319,8 +311,8 @@ describe("postgres-config", () => { "max_connections", "--config", "shared_buffers", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -335,8 +327,8 @@ describe("postgres-config", () => { "--config", "max_connections", "--no-restart", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }, @@ -354,8 +346,8 @@ describe("postgres-config", () => { "--experimental", "--config", "max_connections", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -373,8 +365,8 @@ describe("postgres-config", () => { "--experimental", "--config", "max_connections", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -392,8 +384,8 @@ describe("postgres-config", () => { "--experimental", "--config", "max_connections", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -411,8 +403,8 @@ describe("postgres-config", () => { "--experimental", "--config", "max_connections", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -427,8 +419,7 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "get", "--experimental", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).not.toBe(""); @@ -443,8 +434,7 @@ describe("vanity-subdomains", () => { "--experimental", "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout); @@ -458,8 +448,7 @@ describe("vanity-subdomains", () => { "get", "--experimental", "--debug", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); @@ -475,8 +464,7 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -492,8 +480,7 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -509,8 +496,7 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -526,8 +512,7 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -543,8 +528,7 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -559,8 +543,7 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "myapp", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).not.toBe(""); @@ -577,8 +560,7 @@ describe("vanity-subdomains", () => { "myapp", "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout); @@ -591,8 +573,7 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "check-availability", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); }); @@ -612,8 +593,7 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "taken", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("already taken"); @@ -631,8 +611,7 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "myapp", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -650,8 +629,7 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "myapp", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -669,8 +647,7 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "myapp", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -685,8 +662,8 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "myapp", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Activated vanity subdomain"); @@ -707,8 +684,8 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "taken", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("already taken"); @@ -726,8 +703,8 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "myapp", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -745,8 +722,8 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "myapp", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -764,8 +741,8 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "myapp", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -783,8 +760,8 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "myapp", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -797,8 +774,8 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "delete", "--experimental", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stderr).toContain("Deleted vanity subdomain"); @@ -814,8 +791,8 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "delete", "--experimental", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -831,8 +808,8 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "delete", "--experimental", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -848,8 +825,8 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "delete", "--experimental", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -865,8 +842,8 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "delete", "--experimental", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -877,7 +854,7 @@ describe("vanity-subdomains", () => { describe("encryption", () => { describe("encryption:get-root-key", () => { testBehaviour("renders root encryption key", async ({ run, projectRef }) => { - const result = await run(["encryption", "get-root-key", "--project-ref", projectRef]); + const result = await run(["encryption", "get-root-key", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).not.toBe(""); }); @@ -888,20 +865,13 @@ describe("encryption", () => { "get-root-key", "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); testBehaviour("--debug shows HTTP trace", async ({ run, projectRef }) => { - const result = await run([ - "encryption", - "get-root-key", - "--debug", - "--project-ref", - projectRef, - ]); + const result = await run(["encryption", "get-root-key", "--debug", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); }); @@ -912,7 +882,7 @@ describe("encryption", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["encryption", "get-root-key", "--project-ref", PROJECT_REF]); + const result = await run(["encryption", "get-root-key", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -923,7 +893,7 @@ describe("encryption", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["encryption", "get-root-key", "--project-ref", PROJECT_REF]); + const result = await run(["encryption", "get-root-key", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -934,7 +904,7 @@ describe("encryption", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), }); - const result = await run(["encryption", "get-root-key", "--project-ref", PROJECT_REF]); + const result = await run(["encryption", "get-root-key", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); }); @@ -945,7 +915,7 @@ describe("encryption", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["encryption", "get-root-key", "--project-ref", PROJECT_REF]); + const result = await run(["encryption", "get-root-key", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -956,7 +926,7 @@ describe("encryption", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["encryption", "get-root-key", "--project-ref", PROJECT_REF]); + const result = await run(["encryption", "get-root-key", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); @@ -964,7 +934,12 @@ describe("encryption", () => { describe("encryption:update-root-key", () => { testBehaviour.skipIf(isRecording)("rotates the vault root key", async ({ run, projectRef }) => { - const result = await run(["encryption", "update-root-key", "--project-ref", projectRef]); + const result = await run([ + "encryption", + "update-root-key", + "--yes", + `--remote=${projectRef}`, + ]); expect(result.exitCode).toBe(0); }); @@ -974,7 +949,12 @@ describe("encryption", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["encryption", "update-root-key", "--project-ref", PROJECT_REF]); + const result = await run([ + "encryption", + "update-root-key", + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -985,7 +965,12 @@ describe("encryption", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["encryption", "update-root-key", "--project-ref", PROJECT_REF]); + const result = await run([ + "encryption", + "update-root-key", + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -996,7 +981,12 @@ describe("encryption", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), }); - const result = await run(["encryption", "update-root-key", "--project-ref", PROJECT_REF]); + const result = await run([ + "encryption", + "update-root-key", + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); }); @@ -1007,7 +997,12 @@ describe("encryption", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["encryption", "update-root-key", "--project-ref", PROJECT_REF]); + const result = await run([ + "encryption", + "update-root-key", + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -1018,7 +1013,12 @@ describe("encryption", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["encryption", "update-root-key", "--project-ref", PROJECT_REF]); + const result = await run([ + "encryption", + "update-root-key", + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); @@ -1028,7 +1028,7 @@ describe("encryption", () => { describe("backups", () => { describe("backups:list", () => { testBehaviour("renders backup table with REGION column", async ({ run, projectRef }) => { - const result = await run(["backups", "list", "--project-ref", projectRef]); + const result = await run(["backups", "list", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("REGION"); }); @@ -1036,14 +1036,7 @@ describe("backups", () => { testBehaviour( "returns backup response as JSON with --output json", async ({ run, projectRef }) => { - const result = await run([ - "backups", - "list", - "--output", - "json", - "--project-ref", - projectRef, - ]); + const result = await run(["backups", "list", "--output", "json", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout); expect(parsed).toHaveProperty("region"); @@ -1051,7 +1044,7 @@ describe("backups", () => { ); testBehaviour("--debug shows HTTP trace", async ({ run, projectRef }) => { - const result = await run(["backups", "list", "--debug", "--project-ref", projectRef]); + const result = await run(["backups", "list", "--debug", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); }); @@ -1062,7 +1055,7 @@ describe("backups", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["backups", "list", "--project-ref", PROJECT_REF]); + const result = await run(["backups", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -1073,7 +1066,7 @@ describe("backups", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["backups", "list", "--project-ref", PROJECT_REF]); + const result = await run(["backups", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -1084,7 +1077,7 @@ describe("backups", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), }); - const result = await run(["backups", "list", "--project-ref", PROJECT_REF]); + const result = await run(["backups", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); }); @@ -1095,7 +1088,7 @@ describe("backups", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["backups", "list", "--project-ref", PROJECT_REF]); + const result = await run(["backups", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -1106,7 +1099,7 @@ describe("backups", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["backups", "list", "--project-ref", PROJECT_REF]); + const result = await run(["backups", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); @@ -1121,8 +1114,8 @@ describe("backups", () => { "restore", "-t", String(BACKUP_TIMESTAMP), - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }, @@ -1137,7 +1130,14 @@ describe("backups", () => { body: { message: "recovery time target is out of range" }, }), }); - const result = await run(["backups", "restore", "-t", "0", "--project-ref", PROJECT_REF]); + const result = await run([ + "backups", + "restore", + "-t", + "0", + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("out of range"); }); @@ -1153,8 +1153,8 @@ describe("backups", () => { "restore", "-t", String(BACKUP_TIMESTAMP), - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -1171,8 +1171,8 @@ describe("backups", () => { "restore", "-t", String(BACKUP_TIMESTAMP), - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -1189,8 +1189,8 @@ describe("backups", () => { "restore", "-t", String(BACKUP_TIMESTAMP), - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -1207,8 +1207,8 @@ describe("backups", () => { "restore", "-t", String(BACKUP_TIMESTAMP), - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -1219,7 +1219,7 @@ describe("backups", () => { describe("snippets", () => { describe("snippets:list", () => { testBehaviour("renders snippet table with ID and NAME columns", async ({ run, projectRef }) => { - const result = await run(["snippets", "list", "--project-ref", projectRef]); + const result = await run(["snippets", "list", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("ID"); expect(result.stdout).toContain("NAME"); @@ -1233,8 +1233,7 @@ describe("snippets", () => { "list", "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout); @@ -1243,7 +1242,7 @@ describe("snippets", () => { ); testBehaviour("--debug shows HTTP trace", async ({ run, projectRef }) => { - const result = await run(["snippets", "list", "--debug", "--project-ref", projectRef]); + const result = await run(["snippets", "list", "--debug", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); }); @@ -1254,7 +1253,7 @@ describe("snippets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["snippets", "list", "--project-ref", PROJECT_REF]); + const result = await run(["snippets", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -1265,7 +1264,7 @@ describe("snippets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["snippets", "list", "--project-ref", PROJECT_REF]); + const result = await run(["snippets", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -1276,7 +1275,7 @@ describe("snippets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), }); - const result = await run(["snippets", "list", "--project-ref", PROJECT_REF]); + const result = await run(["snippets", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); }); @@ -1287,7 +1286,7 @@ describe("snippets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["snippets", "list", "--project-ref", PROJECT_REF]); + const result = await run(["snippets", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -1298,7 +1297,7 @@ describe("snippets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["snippets", "list", "--project-ref", PROJECT_REF]); + const result = await run(["snippets", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); @@ -1308,7 +1307,7 @@ describe("snippets", () => { testBehaviour.skipIf(isRecording)( "prints SQL content to stdout", async ({ run, projectRef }) => { - const result = await run(["snippets", "download", SNIPPET_ID, "--project-ref", projectRef]); + const result = await run(["snippets", "download", SNIPPET_ID, `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).not.toBe(""); }, @@ -1320,7 +1319,7 @@ describe("snippets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Snippet not found" } }), }); - const result = await run(["snippets", "download", SNIPPET_ID, "--project-ref", PROJECT_REF]); + const result = await run(["snippets", "download", SNIPPET_ID, `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("not found"); }); @@ -1331,7 +1330,7 @@ describe("snippets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["snippets", "download", SNIPPET_ID, "--project-ref", PROJECT_REF]); + const result = await run(["snippets", "download", SNIPPET_ID, `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -1342,7 +1341,7 @@ describe("snippets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["snippets", "download", SNIPPET_ID, "--project-ref", PROJECT_REF]); + const result = await run(["snippets", "download", SNIPPET_ID, `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -1353,7 +1352,7 @@ describe("snippets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["snippets", "download", SNIPPET_ID, "--project-ref", PROJECT_REF]); + const result = await run(["snippets", "download", SNIPPET_ID, `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); diff --git a/apps/cli-e2e/src/tests/auth.e2e.test.ts b/apps/cli-e2e/src/tests/auth.e2e.test.ts index d2ea7ae761..6172a265e4 100644 --- a/apps/cli-e2e/src/tests/auth.e2e.test.ts +++ b/apps/cli-e2e/src/tests/auth.e2e.test.ts @@ -5,5 +5,5 @@ import { describe, it } from "vitest"; // keychain dialog that blocks the subprocess indefinitely. describe("auth", () => { - it.todo("login and logout tests blocked on Go CLI keyring opt-out (see file comment)"); + it.todo("login and logout tests blocked on keyring opt-out (see file comment)"); }); diff --git a/apps/cli-e2e/src/tests/branches.e2e.test.ts b/apps/cli-e2e/src/tests/branches.e2e.test.ts index a956c1b577..f979199dba 100644 --- a/apps/cli-e2e/src/tests/branches.e2e.test.ts +++ b/apps/cli-e2e/src/tests/branches.e2e.test.ts @@ -7,7 +7,7 @@ const BRANCH_NAME = "my-branch"; describe("branches", () => { describe("branches:list", () => { testBehaviour("renders fixture data in output", async ({ run, projectRef }) => { - const result = await run(["branches", "list", "--project-ref", projectRef]); + const result = await run(["branches", "list", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("NAME"); expect(result.stdout).toContain("STATUS"); @@ -21,8 +21,7 @@ describe("branches", () => { "list", "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout) as unknown[]; @@ -31,7 +30,7 @@ describe("branches", () => { ); testBehaviour("includes debug output with --debug", async ({ run, projectRef }) => { - const result = await run(["branches", "list", "--debug", "--project-ref", projectRef]); + const result = await run(["branches", "list", "--debug", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); }); @@ -42,7 +41,7 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["branches", "list", "--project-ref", PROJECT_REF]); + const result = await run(["branches", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -53,7 +52,7 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["branches", "list", "--project-ref", PROJECT_REF]); + const result = await run(["branches", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -64,7 +63,7 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["branches", "list", "--project-ref", PROJECT_REF]); + const result = await run(["branches", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -75,7 +74,7 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["branches", "list", "--project-ref", PROJECT_REF]); + const result = await run(["branches", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); @@ -83,7 +82,13 @@ describe("branches", () => { describe("branches:create", () => { testBehaviour.skipIf(isRecording)("creates ephemeral branch", async ({ run, projectRef }) => { - const result = await run(["branches", "create", BRANCH_NAME, "--project-ref", projectRef]); + const result = await run([ + "branches", + "create", + BRANCH_NAME, + "--yes", + `--remote=${projectRef}`, + ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Created preview branch:"); }); @@ -94,8 +99,8 @@ describe("branches", () => { "create", BRANCH_NAME, "--persistent", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Created preview branch:"); @@ -107,8 +112,8 @@ describe("branches", () => { "create", BRANCH_NAME, "--with-data", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Created preview branch:"); @@ -120,7 +125,13 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 409, body: { message: "Branch name already in use" } }), }); - const result = await run(["branches", "create", BRANCH_NAME, "--project-ref", PROJECT_REF]); + const result = await run([ + "branches", + "create", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Branch name already in use"); }); @@ -134,7 +145,13 @@ describe("branches", () => { body: { message: "Preview branching is not enabled" }, }), }); - const result = await run(["branches", "create", BRANCH_NAME, "--project-ref", PROJECT_REF]); + const result = await run([ + "branches", + "create", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Preview branching is not enabled"); }); @@ -151,8 +168,8 @@ describe("branches", () => { BRANCH_NAME, "--region", "invalid-region", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("--region"); @@ -164,7 +181,13 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["branches", "create", BRANCH_NAME, "--project-ref", PROJECT_REF]); + const result = await run([ + "branches", + "create", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -175,7 +198,13 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["branches", "create", BRANCH_NAME, "--project-ref", PROJECT_REF]); + const result = await run([ + "branches", + "create", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -186,7 +215,13 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["branches", "create", BRANCH_NAME, "--project-ref", PROJECT_REF]); + const result = await run([ + "branches", + "create", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -196,7 +231,7 @@ describe("branches", () => { testBehaviour.skipIf(isRecording)( "returns single branch details", async ({ run, projectRef }) => { - const result = await run(["branches", "get", BRANCH_NAME, "--project-ref", projectRef]); + const result = await run(["branches", "get", BRANCH_NAME, `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain(BRANCH_NAME); }, @@ -211,8 +246,7 @@ describe("branches", () => { BRANCH_NAME, "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout) as Record; @@ -226,7 +260,7 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Branch not found" } }), }); - const result = await run(["branches", "get", "nonexistent", "--project-ref", PROJECT_REF]); + const result = await run(["branches", "get", "nonexistent", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Branch not found"); }); @@ -237,7 +271,7 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["branches", "get", BRANCH_NAME, "--project-ref", PROJECT_REF]); + const result = await run(["branches", "get", BRANCH_NAME, `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -251,8 +285,8 @@ describe("branches", () => { BRANCH_NAME, "--name", "renamed-branch", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stderr).toContain("Updated preview branch:"); @@ -267,8 +301,8 @@ describe("branches", () => { BRANCH_NAME, "--git-branch", "feature/new", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stderr).toContain("Updated preview branch:"); @@ -287,8 +321,8 @@ describe("branches", () => { "nonexistent", "--name", "new-name", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Branch not found"); @@ -306,8 +340,8 @@ describe("branches", () => { BRANCH_NAME, "--name", "new-name", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -316,7 +350,13 @@ describe("branches", () => { describe("branches:pause", () => { testBehaviour.skipIf(isRecording)("pauses branch successfully", async ({ run, projectRef }) => { - const result = await run(["branches", "pause", BRANCH_NAME, "--project-ref", projectRef]); + const result = await run([ + "branches", + "pause", + BRANCH_NAME, + "--yes", + `--remote=${projectRef}`, + ]); expect(result.exitCode).toBe(0); }); @@ -326,7 +366,13 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Branch not found" } }), }); - const result = await run(["branches", "pause", "nonexistent", "--project-ref", PROJECT_REF]); + const result = await run([ + "branches", + "pause", + "nonexistent", + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Branch not found"); }); @@ -337,7 +383,13 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["branches", "pause", BRANCH_NAME, "--project-ref", PROJECT_REF]); + const result = await run([ + "branches", + "pause", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -347,7 +399,13 @@ describe("branches", () => { testBehaviour.skipIf(isRecording)( "unpauses branch successfully", async ({ run, projectRef }) => { - const result = await run(["branches", "unpause", BRANCH_NAME, "--project-ref", projectRef]); + const result = await run([ + "branches", + "unpause", + BRANCH_NAME, + "--yes", + `--remote=${projectRef}`, + ]); expect(result.exitCode).toBe(0); }, ); @@ -358,7 +416,13 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["branches", "unpause", BRANCH_NAME, "--project-ref", PROJECT_REF]); + const result = await run([ + "branches", + "unpause", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -368,7 +432,13 @@ describe("branches", () => { testBehaviour.skipIf(isRecording)( "deletes branch successfully", async ({ run, projectRef }) => { - const result = await run(["branches", "delete", BRANCH_NAME, "--project-ref", projectRef]); + const result = await run([ + "branches", + "delete", + BRANCH_NAME, + "--yes", + `--remote=${projectRef}`, + ]); expect(result.exitCode).toBe(0); expect(result.stderr).toContain("Deleted preview branch:"); }, @@ -380,7 +450,13 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Branch not found" } }), }); - const result = await run(["branches", "delete", "nonexistent", "--project-ref", PROJECT_REF]); + const result = await run([ + "branches", + "delete", + "nonexistent", + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Branch not found"); }); @@ -391,7 +467,13 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["branches", "delete", BRANCH_NAME, "--project-ref", PROJECT_REF]); + const result = await run([ + "branches", + "delete", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -399,7 +481,7 @@ describe("branches", () => { describe("branches:disable", () => { testBehaviour.skipIf(isRecording)("disables preview branching", async ({ run, projectRef }) => { - const result = await run(["branches", "disable", "--project-ref", projectRef]); + const result = await run(["branches", "disable", "--yes", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Disabled preview branching for project:"); }); @@ -410,7 +492,7 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["branches", "disable", "--project-ref", PROJECT_REF]); + const result = await run(["branches", "disable", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); diff --git a/apps/cli-e2e/src/tests/config-push.e2e.test.ts b/apps/cli-e2e/src/tests/config-push.e2e.test.ts index 617c6158cc..3be38add6e 100644 --- a/apps/cli-e2e/src/tests/config-push.e2e.test.ts +++ b/apps/cli-e2e/src/tests/config-push.e2e.test.ts @@ -59,13 +59,13 @@ enabled = true describe("config push", () => { testBehaviour("reconciles every section against the remote", async ({ run, workspace }) => { writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", "--project-ref", PROJECT_REF]); + const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).toBe(0); }); testBehaviour("emits HTTP trace with --debug", async ({ run, workspace }) => { writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", "--project-ref", PROJECT_REF, "--debug"]); + const result = await run(["config", "push", "--yes", "--debug", `--remote=${PROJECT_REF}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); }); @@ -81,7 +81,7 @@ describe("config push", () => { }); writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", "--project-ref", PROJECT_REF]); + const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("401"); }); @@ -101,7 +101,7 @@ describe("config push", () => { }); writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", "--project-ref", PROJECT_REF]); + const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("403"); }, @@ -118,7 +118,7 @@ describe("config push", () => { }); writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", "--project-ref", PROJECT_REF]); + const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("404"); }); @@ -131,7 +131,7 @@ describe("config push", () => { }); writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", "--project-ref", PROJECT_REF]); + const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("409"); }); @@ -149,7 +149,7 @@ describe("config push", () => { }); writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", "--project-ref", PROJECT_REF]); + const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("422"); expect(result.stderr).toContain("max_rows"); @@ -166,7 +166,7 @@ describe("config push", () => { }); writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", "--project-ref", PROJECT_REF]); + const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("429"); }); @@ -182,7 +182,7 @@ describe("config push", () => { }); writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", "--project-ref", PROJECT_REF]); + const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("500"); }); @@ -195,7 +195,7 @@ describe("config push", () => { }); writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", "--project-ref", PROJECT_REF]); + const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("502"); }); diff --git a/apps/cli-e2e/src/tests/config-toml.e2e.test.ts b/apps/cli-e2e/src/tests/config-toml.e2e.test.ts index e21e572773..5275c838b8 100644 --- a/apps/cli-e2e/src/tests/config-toml.e2e.test.ts +++ b/apps/cli-e2e/src/tests/config-toml.e2e.test.ts @@ -46,7 +46,7 @@ describe("env-in-config-toml", () => { body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["secrets", "list", "--project-ref", PROJECT_REF], { + const result = await run(["secrets", "list", `--remote=${PROJECT_REF}`], { env: ENV_PORTS, }); diff --git a/apps/cli-e2e/src/tests/database-core.e2e.test.ts b/apps/cli-e2e/src/tests/database-core.e2e.test.ts index 0adcb9b5ad..1a4f575c59 100644 --- a/apps/cli-e2e/src/tests/database-core.e2e.test.ts +++ b/apps/cli-e2e/src/tests/database-core.e2e.test.ts @@ -1,23 +1,9 @@ import { mkdirSync, readdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect } from "vitest"; +import { linkProject } from "./link-project.ts"; import { testBehaviour } from "./test-context.ts"; -/** Writes .supabase/.temp/project-ref and a stub pooler-url so --linked - * commands resolve a connection without a live postgres TCP probe (nothing - * listens on db.{ref}.localhost:5432 in the test harness); combined with - * SUPABASE_DB_PASSWORD (set in harness.ts), the command reaches its handler - * and makes the Management API call under test. */ -function linkProject(dir: string, ref: string): void { - const tempDir = join(dir, "supabase", ".temp"); - mkdirSync(tempDir, { recursive: true }); - writeFileSync(join(tempDir, "project-ref"), ref); - writeFileSync( - join(tempDir, "pooler-url"), - `postgresql://postgres.${ref}:[YOUR-PASSWORD]@aws-0-eu-central-1.pooler.supabase.com:6543/postgres`, - ); -} - const TEST_MIGRATION_SQL = "CREATE TABLE IF NOT EXISTS e2e_test_table (id bigint generated always as identity primary key);"; @@ -32,7 +18,7 @@ describe("db advisors", () => { describe("db advisors:security", () => { testBehaviour("returns security advisors", async ({ run, projectRef, workspace }) => { linkProject(workspace.path, projectRef); - const result = await run(["db", "advisors", "--linked", "--type", "security"]); + const result = await run(["db", "advisors", "--type", "security", "--remote"]); expect(result.exitCode).toBe(0); expect(result.stdout.trim()).toBe(""); expect(result.stderr).toContain("No issues found"); @@ -45,11 +31,11 @@ describe("db advisors", () => { const result = await run([ "db", "advisors", - "--linked", "--type", "security", "--fail-on", "error", + "--remote", ]); expect(result.exitCode).toBe(0); }, @@ -62,7 +48,7 @@ describe("db advisors", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["db", "advisors", "--linked", "--type", "security"]); + const result = await run(["db", "advisors", "--type", "security", "--remote"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -74,7 +60,7 @@ describe("db advisors", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["db", "advisors", "--linked", "--type", "security"]); + const result = await run(["db", "advisors", "--type", "security", "--remote"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -86,7 +72,7 @@ describe("db advisors", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["db", "advisors", "--linked", "--type", "security"]); + const result = await run(["db", "advisors", "--type", "security", "--remote"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -98,7 +84,7 @@ describe("db advisors", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["db", "advisors", "--linked", "--type", "security"]); + const result = await run(["db", "advisors", "--type", "security", "--remote"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); @@ -107,7 +93,7 @@ describe("db advisors", () => { describe("db advisors:performance", () => { testBehaviour("returns performance advisors", async ({ run, projectRef, workspace }) => { linkProject(workspace.path, projectRef); - const result = await run(["db", "advisors", "--linked", "--type", "performance"]); + const result = await run(["db", "advisors", "--type", "performance", "--remote"]); expect(result.exitCode).toBe(0); if (result.stdout.trim()) { const parsed = JSON.parse(result.stdout) as unknown[]; @@ -121,19 +107,19 @@ describe("db advisors", () => { describe("db advisors:all", () => { testBehaviour("returns advisors with --type all", async ({ run, projectRef, workspace }) => { linkProject(workspace.path, projectRef); - const result = await run(["db", "advisors", "--linked", "--type", "all"]); + const result = await run(["db", "advisors", "--type", "all", "--remote"]); expect(result.exitCode).toBe(0); }); }); }); describe("db query", () => { - describe("db query:linked", () => { + describe("db query:remote", () => { testBehaviour( "returns SELECT 1 result in table format", async ({ run, projectRef, workspace }) => { linkProject(workspace.path, projectRef); - const result = await run(["db", "query", "--linked", "SELECT 1"]); + const result = await run(["db", "query", "SELECT 1", "--read-only", "--remote"]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("1"); }, @@ -141,7 +127,15 @@ describe("db query", () => { testBehaviour("returns JSON with --output json", async ({ run, projectRef, workspace }) => { linkProject(workspace.path, projectRef); - const result = await run(["db", "query", "--linked", "--output", "json", "SELECT 1"]); + const result = await run([ + "db", + "query", + "--output", + "json", + "SELECT 1", + "--read-only", + "--remote", + ]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout) as unknown; // In agent mode (CLAUDECODE env set) the output is wrapped in {warning, boundary, rows}. @@ -158,7 +152,7 @@ describe("db query", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["db", "query", "--linked", "SELECT 1"]); + const result = await run(["db", "query", "SELECT 1", "--read-only", "--remote"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -170,7 +164,7 @@ describe("db query", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["db", "query", "--linked", "SELECT 1"]); + const result = await run(["db", "query", "SELECT 1", "--read-only", "--remote"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -184,7 +178,7 @@ describe("db push", () => { async ({ run, projectRef, workspace }) => { linkProject(workspace.path, projectRef); seedMigration(workspace.path); - const result = await run(["db", "push", "--dry-run"]); + const result = await run(["db", "push", "--dry-run", "--remote"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("connect"); }, @@ -192,19 +186,19 @@ describe("db push", () => { }); describe("db push:local", () => { - testBehaviour("exits non-zero on connection refused with --local", async ({ run }) => { - const result = await run(["db", "push", "--local"]); + testBehaviour("exits non-zero on connection refused without --remote", async ({ run }) => { + const result = await run(["db", "push"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("connect"); }); }); - describe("db push:linked", () => { + describe("db push:remote", () => { testBehaviour( - "exits non-zero on connection refused with --linked", + "exits non-zero on connection refused with --remote", async ({ run, projectRef, workspace }) => { linkProject(workspace.path, projectRef); - const result = await run(["db", "push"]); + const result = await run(["db", "push", "--remote"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("connect"); }, @@ -213,38 +207,38 @@ describe("db push", () => { }); describe("db pull", () => { - testBehaviour("exits non-zero on connection refused with --local", async ({ run }) => { - const result = await run(["db", "pull", "--local"]); + testBehaviour("exits non-zero on connection refused without --remote", async ({ run }) => { + const result = await run(["db", "pull"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("connect"); }); }); describe("db lint", () => { - testBehaviour("exits non-zero on connection refused with --local", async ({ run }) => { - const result = await run(["db", "lint", "--local"]); + testBehaviour("exits non-zero on connection refused without --remote", async ({ run }) => { + const result = await run(["db", "lint"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("connect"); }); }); describe("db dump", () => { - testBehaviour("exits non-zero on connection refused with --local", async ({ run }) => { - const result = await run(["db", "dump", "--local"]); + testBehaviour("exits non-zero on connection refused without --remote", async ({ run }) => { + const result = await run(["db", "dump"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("connect"); }); testBehaviour("exits non-zero when --role-only and --data-only are both set", async ({ run }) => { - const result = await run(["db", "dump", "--local", "--role-only", "--data-only"]); + const result = await run(["db", "dump", "--role-only", "--data-only"]); expect(result.exitCode).not.toBe(0); expect(result.stderr.toLowerCase()).toMatch(/role.only|data.only|mutually exclusive/); }); }); describe("db reset", () => { - testBehaviour("exits non-zero on connection refused with --local", async ({ run }) => { - const result = await run(["db", "reset", "--local"]); + testBehaviour("exits non-zero on connection refused without --remote", async ({ run }) => { + const result = await run(["db", "reset"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toMatch(/connect|not running/i); }); @@ -268,8 +262,8 @@ describe("test new", () => { }); describe("test db", () => { - testBehaviour("exits non-zero on connection refused with --local", async ({ run }) => { - const result = await run(["test", "db", "--local"]); + testBehaviour("exits non-zero on connection refused without --remote", async ({ run }) => { + const result = await run(["test", "db"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("connect"); }); diff --git a/apps/cli-e2e/src/tests/domains.e2e.test.ts b/apps/cli-e2e/src/tests/domains.e2e.test.ts index 72cb5f37bc..e2244691e6 100644 --- a/apps/cli-e2e/src/tests/domains.e2e.test.ts +++ b/apps/cli-e2e/src/tests/domains.e2e.test.ts @@ -19,21 +19,21 @@ describe("domains", () => { }), }); - const result = await run(["domains", "get", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); }); testBehaviour.skipIf(isRecording)("no custom domain", async ({ run }) => { - const result = await run(["domains", "get", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); }); testBehaviour.skipIf(isRecording)("pending verification", async ({ run }) => { - const result = await run(["domains", "get", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toContain( @@ -42,7 +42,7 @@ describe("domains", () => { }); testBehaviour.skipIf(isRecording)("verification completed", async ({ run }) => { - const result = await run(["domains", "get", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toContain( @@ -51,14 +51,7 @@ describe("domains", () => { }); testBehaviour.skipIf(isRecording)("domain activated", async ({ run }) => { - const result = await run([ - "domains", - "get", - "--project-ref", - PROJECT_REF, - "--output", - "json", - ]); + const result = await run(["domains", "get", "--output", "json", `--remote=${PROJECT_REF}`]); expect(result.exitCode).toBe(0); @@ -98,7 +91,7 @@ describe("domains", () => { }), }); - const result = await run(["domains", "get", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("403"); }); @@ -113,7 +106,7 @@ describe("domains", () => { }), }); - const result = await run(["domains", "get", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("401"); }); @@ -128,7 +121,7 @@ describe("domains", () => { }), }); - const result = await run(["domains", "get", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("429"); }); @@ -143,7 +136,7 @@ describe("domains", () => { }), }); - const result = await run(["domains", "get", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("500"); @@ -159,7 +152,7 @@ describe("domains", () => { }), }); - const result = await run(["domains", "get", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("502"); @@ -168,21 +161,21 @@ describe("domains", () => { describe("domains:reverify", () => { testBehaviour("custom domain disabled", async ({ run }) => { - const result = await run(["domains", "reverify", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "reverify", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); }); testBehaviour.skipIf(isRecording)("no custom domain", async ({ run }) => { - const result = await run(["domains", "reverify", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "reverify", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); }); testBehaviour.skipIf(isRecording)("pending verification", async ({ run }) => { - const result = await run(["domains", "reverify", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "reverify", `--remote=${PROJECT_REF}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toContain( @@ -191,7 +184,7 @@ describe("domains", () => { }); testBehaviour.skipIf(isRecording)("verification completed", async ({ run }) => { - const result = await run(["domains", "reverify", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "reverify", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); @@ -199,28 +192,34 @@ describe("domains", () => { }); describe("domains:activate", () => { testBehaviour("custom domain disabled", async ({ run }) => { - const result = await run(["domains", "activate", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "activate", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); }); testBehaviour.skipIf(isRecording)("no custom domain", async ({ run }) => { - const result = await run(["domains", "activate", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "activate", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); }); testBehaviour.skipIf(isRecording)("pending verification", async ({ run }) => { - const result = await run(["domains", "activate", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "activate", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); }); testBehaviour.skipIf(isRecording)("pending verification in debug mode", async ({ run }) => { - const result = await run(["domains", "activate", "--project-ref", PROJECT_REF, "--debug"]); + const result = await run([ + "domains", + "activate", + "--debug", + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toMatch(/HTTP.*POST:/); @@ -228,7 +227,7 @@ describe("domains", () => { }); testBehaviour.skipIf(isRecording)("verification completed", async ({ run }) => { - const result = await run(["domains", "activate", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "activate", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toContain(`completed`); @@ -236,7 +235,7 @@ describe("domains", () => { }); testBehaviour.skipIf(isRecording)("domain activated", async ({ run }) => { - const result = await run(["domains", "activate", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "activate", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); @@ -245,21 +244,21 @@ describe("domains", () => { describe("domains:delete", () => { testBehaviour("custom domain disabled", async ({ run }) => { - const result = await run(["domains", "delete", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "delete", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); }); testBehaviour.skipIf(isRecording)("no custom domain", async ({ run }) => { - const result = await run(["domains", "delete", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "delete", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); }); testBehaviour.skipIf(isRecording)("pending verification", async ({ run }) => { - const result = await run(["domains", "delete", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "delete", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).toBe(0); }); diff --git a/apps/cli-e2e/src/tests/functions.e2e.test.ts b/apps/cli-e2e/src/tests/functions.e2e.test.ts index f39a423806..af2a08db8e 100644 --- a/apps/cli-e2e/src/tests/functions.e2e.test.ts +++ b/apps/cli-e2e/src/tests/functions.e2e.test.ts @@ -7,28 +7,21 @@ const FUNCTION_NAME = "hello-world"; describe("functions", () => { describe("functions:list", () => { testBehaviour("renders fixture data in output", async ({ run, projectRef }) => { - const result = await run(["functions", "list", "--project-ref", projectRef]); + const result = await run(["functions", "list", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("NAME"); expect(result.stdout).toContain("STATUS"); }); testBehaviour("returns json output with --output json", async ({ run, projectRef }) => { - const result = await run([ - "functions", - "list", - "--output", - "json", - "--project-ref", - projectRef, - ]); + const result = await run(["functions", "list", "--output", "json", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout) as unknown[]; expect(Array.isArray(parsed)).toBe(true); }); testBehaviour("includes debug output with --debug", async ({ run, projectRef }) => { - const result = await run(["functions", "list", "--debug", "--project-ref", projectRef]); + const result = await run(["functions", "list", "--debug", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); }); @@ -39,7 +32,7 @@ describe("functions", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["functions", "list", "--project-ref", PROJECT_REF]); + const result = await run(["functions", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); @@ -49,7 +42,7 @@ describe("functions", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["functions", "list", "--project-ref", PROJECT_REF]); + const result = await run(["functions", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -60,7 +53,7 @@ describe("functions", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["functions", "list", "--project-ref", PROJECT_REF]); + const result = await run(["functions", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -71,15 +64,15 @@ describe("functions", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["functions", "list", "--project-ref", PROJECT_REF]); + const result = await run(["functions", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); }); describe("functions:deploy", () => { - // Deploy requires the Go binary to bundle function files locally before any API call, - // so error injection tests pre-create the function with `functions new` first. + // Deploy bundles function files locally before any API call, so error injection tests + // pre-create the function with `functions new` first. testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { await run(["functions", "new", FUNCTION_NAME]); @@ -93,8 +86,8 @@ describe("functions", () => { "deploy", FUNCTION_NAME, "--use-api", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -112,8 +105,8 @@ describe("functions", () => { "deploy", FUNCTION_NAME, "--use-api", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -134,8 +127,8 @@ describe("functions", () => { "deploy", FUNCTION_NAME, "--use-api", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -153,8 +146,8 @@ describe("functions", () => { "deploy", FUNCTION_NAME, "--use-api", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -169,8 +162,8 @@ describe("functions", () => { "functions", "delete", FUNCTION_NAME, - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Deleted Function"); @@ -187,8 +180,8 @@ describe("functions", () => { "functions", "delete", FUNCTION_NAME, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("does not exist"); @@ -204,8 +197,8 @@ describe("functions", () => { "functions", "delete", FUNCTION_NAME, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -221,8 +214,8 @@ describe("functions", () => { "functions", "delete", FUNCTION_NAME, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -238,8 +231,8 @@ describe("functions", () => { "functions", "delete", FUNCTION_NAME, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -255,8 +248,8 @@ describe("functions", () => { "functions", "delete", FUNCTION_NAME, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -272,8 +265,7 @@ describe("functions", () => { "download", FUNCTION_NAME, "--use-api", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stderr).toContain("Downloaded Function"); @@ -291,8 +283,7 @@ describe("functions", () => { "download", FUNCTION_NAME, "--use-api", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -309,8 +300,7 @@ describe("functions", () => { "download", FUNCTION_NAME, "--use-api", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -327,8 +317,7 @@ describe("functions", () => { "download", FUNCTION_NAME, "--use-api", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Function not found"); @@ -345,8 +334,7 @@ describe("functions", () => { "download", FUNCTION_NAME, "--use-api", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); diff --git a/apps/cli-e2e/src/tests/gen.e2e.test.ts b/apps/cli-e2e/src/tests/gen.e2e.test.ts index 5362a5bf12..95a24b5eec 100644 --- a/apps/cli-e2e/src/tests/gen.e2e.test.ts +++ b/apps/cli-e2e/src/tests/gen.e2e.test.ts @@ -14,7 +14,7 @@ describe("gen", () => { async ({ run, projectRef }) => { // #5212 — with CLAUDECODE=1, piped/redirected output must not include the // plugin hint (would break `gen types > file.ts` and similar captures). - const result = await run(["gen", "types", "--project-id", projectRef], { + const result = await run(["gen", "types", `--remote=${projectRef}`], { env: { CLAUDECODE: "1" }, }); expect(result.exitCode).toBe(0); @@ -28,7 +28,7 @@ describe("gen", () => { testBehaviour.skipIf(isRecording)( "includes debug output with --debug", async ({ run, projectRef }) => { - const result = await run(["gen", "types", "--debug", "--project-id", projectRef]); + const result = await run(["gen", "types", "--debug", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); }, @@ -40,7 +40,7 @@ describe("gen", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["gen", "types", "--project-id", PROJECT_REF]); + const result = await run(["gen", "types", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -51,7 +51,7 @@ describe("gen", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["gen", "types", "--project-id", PROJECT_REF]); + const result = await run(["gen", "types", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -62,15 +62,20 @@ describe("gen", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), }); - const result = await run(["gen", "types", "--project-id", PROJECT_REF]); + const result = await run(["gen", "types", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); }); - testBehaviour("exits non-zero with no data source specified", async ({ runNoProjectId }) => { - const result = await runNoProjectId(["gen", "types"]); - expect(result.exitCode).not.toBe(0); - }); + testBehaviour( + "exits non-zero when the local stack is not running", + async ({ runNoProjectId }) => { + const result = await runNoProjectId(["gen", "types"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Target: local stack"); + expect(result.stderr).toContain("supabase start is not running"); + }, + ); }); describe("gen:signing-key", () => { diff --git a/apps/cli-e2e/src/tests/go-binary-surface.e2e.test.ts b/apps/cli-e2e/src/tests/go-binary-surface.e2e.test.ts deleted file mode 100644 index fc0b9acfe0..0000000000 --- a/apps/cli-e2e/src/tests/go-binary-surface.e2e.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterAll, beforeAll, describe, expect, test } from "vitest"; - -// The bundled `supabase-go` binary retains only the commands the TypeScript -// CLI's `GoProxy` can spawn; every other Go command was deleted. TS -// integration tests stub that subprocess boundary, so nothing else notices if -// a still-reachable Go command gets trimmed away by mistake. -// -// This suite enumerates every argv shape the TS side can hand to `GoProxy` -// and asserts the built `supabase-go` binary still resolves it. If this -// fails, either the TS spawn surface grew (add the command to the retained -// set in `apps/cli-go`) or the trim cut too deep (restore the command). -// -// `SUPABASE_GO_BINARY` is only set to a freshly built binary in CI (see -// `.github/workflows/test.yml`); locally this whole suite no-ops. -const GO_BINARY = process.env["SUPABASE_GO_BINARY"]; - -describe.skipIf(GO_BINARY === undefined)("go binary spawn surface (CLI-1970)", () => { - const binary = GO_BINARY as string; - - let workspaceDir: string; - let bogusProfilePath: string; - - beforeAll(() => { - workspaceDir = mkdtempSync(join(tmpdir(), "cli-e2e-go-binary-surface-")); - - // The upgrade check hits the real GitHub releases API on every invocation - // that returns a nil error (e.g. every `--help` call below), unless a - // `supabase/.temp/cli-latest` cache file already exists and is less than - // 10h old. Pre-seeding it here keeps this suite hermetic. - mkdirSync(join(workspaceDir, "supabase", ".temp"), { recursive: true }); - writeFileSync(join(workspaceDir, "supabase", ".temp", "cli-latest"), "v0.0.0"); - - // A bogus `--profile` for the two Management-API-gated delegates (`gen - // keys`, `functions download --legacy-bundle`): the unique profile name - // can never match a real stored credential, and the unreachable api_url - // means even a stray match still fails at connect. - bogusProfilePath = join(workspaceDir, "profile.yaml"); - writeFileSync( - bogusProfilePath, - [ - "name: cli-e2e-go-binary-surface-guard", - 'api_url: "http://127.0.0.1:1"', - 'dashboard_url: "http://127.0.0.1:1"', - "project_host: localhost", - ].join("\n"), - ); - }); - - afterAll(() => { - rmSync(workspaceDir, { recursive: true, force: true }); - }); - - function runGo(args: ReadonlyArray, envOverrides: Record = {}) { - const result = Bun.spawnSync([binary, ...args], { - cwd: workspaceDir, - env: { - PATH: process.env["PATH"] ?? "", - HOME: workspaceDir, - SUPABASE_HOME: workspaceDir, - // Belt-and-braces: no command exercised here should ever reach a - // real Docker daemon or send real telemetry. - DOCKER_HOST: "tcp://127.0.0.1:1", - SUPABASE_TELEMETRY_DISABLED: "1", - ...envOverrides, - }, - stdout: "pipe", - stderr: "pipe", - }); - return { - exitCode: result.exitCode, - stdout: result.stdout.toString(), - stderr: result.stderr.toString(), - }; - } - - // The complete spawn surface, mirrored from `GoProxy` call sites: - // - db diff (diff.handler.ts, `--use-pg-schema` delegate path) - // - db branch create|delete|list|switch (thin proxies) - // - db remote changes (thin proxy) - // - gen keys (keys.handler.ts) - // - functions download (shared/functions/download.ts, `--legacy-bundle`) - const RETAINED_COMMAND_PATHS: ReadonlyArray> = [ - ["db", "diff"], - ["db", "branch", "create"], - ["db", "branch", "delete"], - ["db", "branch", "list"], - ["db", "branch", "switch"], - ["db", "remote", "changes"], - ["gen", "keys"], - ["functions", "download"], - ]; - - describe("resolves every retained command path", () => { - for (const path of RETAINED_COMMAND_PATHS) { - test(`supabase-go ${path.join(" ")} --help exits 0`, () => { - const { exitCode, stdout, stderr } = runGo([...path, "--help"]); - const output = stdout + stderr; - expect(exitCode).toBe(0); - expect(output).not.toContain("unknown command"); - // A deleted nested subcommand doesn't error "unknown command": cobra - // only rejects unmatched args on the root command, so `db diff --help` - // with `diff` deleted would fall through to `db`'s own help (exit 0). - // Asserting the full command path appears in the usage output closes - // that gap — cobra only prints it once the whole path has resolved. - expect(output).toContain(`supabase ${path.join(" ")}`); - }, 5_000); - } - }); - - describe("accepts the exact argv shape the TS proxy builds", () => { - // `db diff` always provisions a Docker shadow database first, so the bogus - // DOCKER_HOST is what makes this fail fast, not the bogus --db-url. - test("db diff --use-pg-schema", () => { - const { exitCode, stderr } = runGo([ - "db", - "diff", - "--use-pg-schema", - "--db-url", - "postgresql://u:p@127.0.0.1:1/x", - "--schema", - "public", - ]); - expect(exitCode).toBe(1); - expect(stderr).not.toMatch(/unknown flag|invalid argument/i); - }, 5_000); - - // The only invocation in this suite exercising the full global-flag set at - // once. Also provisions a Docker shadow first, so DOCKER_HOST trips it. - test("db remote changes (full global flag set)", () => { - const { exitCode, stderr } = runGo([ - "--output", - "json", - "--profile", - "supabase-staging", - "--debug", - "--workdir", - workspaceDir, - "--experimental", - "--network-id", - "cli-e2e-test-net", - "--yes", - "--dns-resolver", - "https", - "--create-ticket", - "--agent", - "no", - "db", - "remote", - "changes", - "--db-url", - "postgresql://u:p@127.0.0.1:1/x", - "--schema", - "public", - ]); - expect(exitCode).toBe(1); - expect(stderr).not.toMatch(/unknown flag|invalid argument/i); - }, 5_000); - - // Gated behind a login check before any network call, so an isolated - // SUPABASE_HOME plus the bogus --profile fails fast without reaching a real API. - test("gen keys", () => { - const { exitCode, stderr } = runGo([ - "gen", - "keys", - "--project-ref", - "abcdefghijklmnopqrst", - "--override-name", - "db.host=CUSTOM_DB_HOST", - "--experimental", - "--profile", - bogusProfilePath, - ]); - expect(exitCode).toBe(1); - expect(stderr).not.toMatch(/unknown flag|invalid argument/i); - }, 5_000); - - // Same login-gate fail-fast as `gen keys`. - test("functions download --legacy-bundle", () => { - const { exitCode, stderr } = runGo([ - "functions", - "download", - "my-function", - "--project-ref", - "abcdefghijklmnopqrst", - "--legacy-bundle", - "--profile", - bogusProfilePath, - ]); - expect(exitCode).toBe(1); - expect(stderr).not.toMatch(/unknown flag|invalid argument/i); - }, 5_000); - }); - - describe("negative control: a deleted command still reports unknown", () => { - // `["db", "start"]` isn't used here even though it's a deleted command: - // like the nested-subcommand case above, `db` would fall through to its - // own help (exit 0) instead of erroring. Only a deleted top-level command - // reliably reproduces "unknown command". - for (const deletedCommand of ["inspect", "start"]) { - test(`supabase-go ${deletedCommand} reports unknown command`, () => { - const { exitCode, stderr } = runGo([deletedCommand]); - expect(exitCode).toBe(1); - expect(stderr).toContain("unknown command"); - }, 5_000); - } - }); -}); diff --git a/apps/cli-e2e/src/tests/inspect.e2e.test.ts b/apps/cli-e2e/src/tests/inspect.e2e.test.ts index 9f5014dab1..bdb9bd5311 100644 --- a/apps/cli-e2e/src/tests/inspect.e2e.test.ts +++ b/apps/cli-e2e/src/tests/inspect.e2e.test.ts @@ -37,17 +37,19 @@ const SUBCOMMANDS = [ ] as const; describe("inspect:flags", () => { - testBehaviour("rejects --db-url with --local", async ({ run, workspace, pgMockPort }) => { + testBehaviour("rejects --remote with --local", async ({ run, workspace, pgMockPort }) => { setupInspectWorkspace(workspace.path, pgMockPort); const result = await run([ "inspect", "db", "db-stats", - "--db-url", - "postgresql://postgres:postgres@localhost:5432/postgres", "--local", + "--remote=postgresql://postgres:postgres@localhost:5432/postgres", ]); expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain( + "Only one of --remote, --linked, --db-url and --local can be set; got --remote and --local.", + ); }); }); @@ -58,7 +60,7 @@ for (const { name, fixtureKey, assertValue } of SUBCOMMANDS) { async ({ run, workspace, apiUrl, pgMockPort }) => { setupInspectWorkspace(workspace.path, pgMockPort); await setPgFixture(apiUrl, fixtureKey); - const result = await run(["inspect", "db", name, "--local"]); + const result = await run(["inspect", "db", name]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain(assertValue); }, @@ -69,8 +71,7 @@ for (const { name, fixtureKey, assertValue } of SUBCOMMANDS) { "inspect", "db", name, - "--db-url", - "postgresql://postgres:postgres@localhost:1/postgres", + "--remote=postgresql://postgres:postgres@localhost:1/postgres", ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).not.toBe(""); @@ -82,7 +83,7 @@ describe("inspect:report", () => { testBehaviour("saves CSV files on success", async ({ run, workspace, pgMockPort }) => { setupInspectWorkspace(workspace.path, pgMockPort); const outDir = join(workspace.path, "report-out"); - const result = await run(["inspect", "report", "--local", "--output-dir", outDir]); + const result = await run(["inspect", "report", "--output-dir", outDir]); expect(result.exitCode).toBe(0); expect(result.stderr).toContain("Reports saved to"); const subdirs = readdirSync(outDir); @@ -97,8 +98,7 @@ describe("inspect:report", () => { const result = await run([ "inspect", "report", - "--db-url", - "postgresql://postgres:postgres@localhost:1/postgres", + "--remote=postgresql://postgres:postgres@localhost:1/postgres", ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).not.toBe(""); diff --git a/apps/cli-e2e/src/tests/link-project.ts b/apps/cli-e2e/src/tests/link-project.ts new file mode 100644 index 0000000000..513658c3c7 --- /dev/null +++ b/apps/cli-e2e/src/tests/link-project.ts @@ -0,0 +1,13 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +/** Writes `project-ref` and a stub `pooler-url` so `--remote` skips the live Postgres probe. */ +export function linkProject(dir: string, ref: string): void { + const tempDir = join(dir, "supabase", ".temp"); + mkdirSync(tempDir, { recursive: true }); + writeFileSync(join(tempDir, "project-ref"), ref); + writeFileSync( + join(tempDir, "pooler-url"), + `postgresql://postgres.${ref}:[YOUR-PASSWORD]@aws-0-eu-central-1.pooler.supabase.com:6543/postgres`, + ); +} diff --git a/apps/cli-e2e/src/tests/migrations.e2e.test.ts b/apps/cli-e2e/src/tests/migrations.e2e.test.ts index 3a0c349e42..3180c38ae3 100644 --- a/apps/cli-e2e/src/tests/migrations.e2e.test.ts +++ b/apps/cli-e2e/src/tests/migrations.e2e.test.ts @@ -1,4 +1,4 @@ -import { readdirSync } from "node:fs"; +import { mkdirSync, readdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect } from "vitest"; import { testBehaviour } from "./test-context.ts"; @@ -26,7 +26,7 @@ describe("migrations", () => { describe("migration:list", () => { testBehaviour("exits non-zero on connection refused", async ({ run }) => { - const result = await run(["migration", "list", "--local"]); + const result = await run(["migration", "list"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("failed to connect"); }); @@ -34,7 +34,7 @@ describe("migrations", () => { describe("migration:up", () => { testBehaviour("exits non-zero on connection refused", async ({ run }) => { - const result = await run(["migration", "up", "--local"]); + const result = await run(["migration", "up"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("failed to connect"); }); @@ -42,13 +42,13 @@ describe("migrations", () => { describe("migration:down", () => { testBehaviour("exits non-zero on connection refused", async ({ run }) => { - const result = await run(["migration", "down", "--local"]); + const result = await run(["migration", "down"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("failed to connect"); }); testBehaviour("exits non-zero on connection refused with --last 2", async ({ run }) => { - const result = await run(["migration", "down", "--last", "2", "--local"]); + const result = await run(["migration", "down", "--last", "2"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("failed to connect"); }); @@ -56,22 +56,18 @@ describe("migrations", () => { describe("migration:repair", () => { testBehaviour("exits non-zero when --status flag is missing", async ({ run }) => { - const result = await run(["migration", "repair", "--local", "20230101000000"]); + const result = await run(["migration", "repair", "20230101000000"]); expect(result.exitCode).not.toBe(0); // A missing required flag's error prints once, without a duplicate // usage dump, and spells the flag name without its `--` prefix. expect(result.stderr).toContain('"status" not set'); }); - testBehaviour("exits non-zero on connection refused", async ({ run }) => { - const result = await run([ - "migration", - "repair", - "--status", - "applied", - "--local", - "20230101000000", - ]); + testBehaviour("exits non-zero on connection refused", async ({ run, workspace }) => { + const migrationsDir = join(workspace.path, "supabase", "migrations"); + mkdirSync(migrationsDir, { recursive: true }); + writeFileSync(join(migrationsDir, "20230101000000_x.sql"), "select 1;\n"); + const result = await run(["migration", "repair", "--status", "applied", "20230101000000"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("failed to connect"); }); @@ -79,7 +75,7 @@ describe("migrations", () => { describe("migration:squash", () => { testBehaviour("exits non-zero on connection refused", async ({ run }) => { - const result = await run(["migration", "squash", "--local"]); + const result = await run(["migration", "squash"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).not.toBe(""); }); @@ -87,7 +83,7 @@ describe("migrations", () => { describe("migration:fetch", () => { testBehaviour("exits non-zero on connection refused", async ({ run }) => { - const result = await run(["migration", "fetch", "--local"]); + const result = await run(["migration", "fetch"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("failed to connect"); }); diff --git a/apps/cli-e2e/src/tests/network-security.e2e.test.ts b/apps/cli-e2e/src/tests/network-security.e2e.test.ts index 8cff53bcc4..5e19ae3803 100644 --- a/apps/cli-e2e/src/tests/network-security.e2e.test.ts +++ b/apps/cli-e2e/src/tests/network-security.e2e.test.ts @@ -5,13 +5,7 @@ import { testBehaviour } from "./test-context.ts"; describe("network-bans", () => { describe("network-bans:get", () => { testBehaviour("renders fixture data in output", async ({ run, projectRef }) => { - const result = await run([ - "network-bans", - "get", - "--experimental", - "--project-ref", - projectRef, - ]); + const result = await run(["network-bans", "get", "--experimental", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); }); @@ -22,8 +16,7 @@ describe("network-bans", () => { "--experimental", "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -34,8 +27,7 @@ describe("network-bans", () => { "get", "--experimental", "--debug", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); @@ -51,8 +43,7 @@ describe("network-bans", () => { "network-bans", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -68,8 +59,7 @@ describe("network-bans", () => { "network-bans", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -85,8 +75,7 @@ describe("network-bans", () => { "network-bans", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -102,8 +91,7 @@ describe("network-bans", () => { "network-bans", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -119,8 +107,7 @@ describe("network-bans", () => { "network-bans", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -135,8 +122,8 @@ describe("network-bans", () => { "--experimental", "--db-unban-ip", "1.2.3.4", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -150,8 +137,8 @@ describe("network-bans", () => { "1.2.3.4", "--db-unban-ip", "5.6.7.8", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -163,8 +150,8 @@ describe("network-bans", () => { "--experimental", "--db-unban-ip", "invalid-ip", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("invalid IP address"); @@ -182,8 +169,8 @@ describe("network-bans", () => { "--experimental", "--db-unban-ip", "1.2.3.4", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -201,8 +188,8 @@ describe("network-bans", () => { "--experimental", "--db-unban-ip", "1.2.3.4", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -217,8 +204,7 @@ describe("network-restrictions", () => { "network-restrictions", "get", "--experimental", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -230,8 +216,7 @@ describe("network-restrictions", () => { "--experimental", "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -246,8 +231,7 @@ describe("network-restrictions", () => { "network-restrictions", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -263,8 +247,7 @@ describe("network-restrictions", () => { "network-restrictions", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -280,8 +263,7 @@ describe("network-restrictions", () => { "network-restrictions", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -297,8 +279,7 @@ describe("network-restrictions", () => { "network-restrictions", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -314,8 +295,7 @@ describe("network-restrictions", () => { "network-restrictions", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -330,8 +310,8 @@ describe("network-restrictions", () => { "--experimental", "--db-allow-cidr", "0.0.0.0/0", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -344,8 +324,8 @@ describe("network-restrictions", () => { "--append", "--db-allow-cidr", "8.8.8.0/24", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -358,8 +338,8 @@ describe("network-restrictions", () => { "--bypass-cidr-checks", "--db-allow-cidr", "0.0.0.0/0", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -371,8 +351,8 @@ describe("network-restrictions", () => { "--experimental", "--db-allow-cidr", "not-a-cidr", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("failed to parse IP"); @@ -390,8 +370,8 @@ describe("network-restrictions", () => { "--experimental", "--db-allow-cidr", "0.0.0.0/0", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -409,8 +389,8 @@ describe("network-restrictions", () => { "--experimental", "--db-allow-cidr", "0.0.0.0/0", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -428,8 +408,8 @@ describe("network-restrictions", () => { "--experimental", "--db-allow-cidr", "0.0.0.0/0", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -447,8 +427,8 @@ describe("network-restrictions", () => { "--experimental", "--db-allow-cidr", "0.0.0.0/0", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -463,8 +443,7 @@ describe("ssl-enforcement", () => { "ssl-enforcement", "get", "--experimental", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -476,8 +455,7 @@ describe("ssl-enforcement", () => { "--experimental", "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -492,8 +470,7 @@ describe("ssl-enforcement", () => { "ssl-enforcement", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -509,8 +486,7 @@ describe("ssl-enforcement", () => { "ssl-enforcement", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -526,8 +502,7 @@ describe("ssl-enforcement", () => { "ssl-enforcement", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -543,8 +518,7 @@ describe("ssl-enforcement", () => { "ssl-enforcement", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -560,8 +534,7 @@ describe("ssl-enforcement", () => { "ssl-enforcement", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -575,8 +548,8 @@ describe("ssl-enforcement", () => { "update", "--experimental", "--enable-db-ssl-enforcement", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -587,8 +560,8 @@ describe("ssl-enforcement", () => { "update", "--experimental", "--disable-db-ssl-enforcement", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -600,8 +573,8 @@ describe("ssl-enforcement", () => { "--experimental", "--enable-db-ssl-enforcement", "--disable-db-ssl-enforcement", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); }); @@ -611,8 +584,8 @@ describe("ssl-enforcement", () => { "ssl-enforcement", "update", "--experimental", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); }); @@ -628,8 +601,8 @@ describe("ssl-enforcement", () => { "update", "--experimental", "--enable-db-ssl-enforcement", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -646,8 +619,8 @@ describe("ssl-enforcement", () => { "update", "--experimental", "--enable-db-ssl-enforcement", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -664,8 +637,8 @@ describe("ssl-enforcement", () => { "update", "--experimental", "--enable-db-ssl-enforcement", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -682,8 +655,8 @@ describe("ssl-enforcement", () => { "update", "--experimental", "--enable-db-ssl-enforcement", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -700,8 +673,8 @@ describe("ssl-enforcement", () => { "update", "--experimental", "--enable-db-ssl-enforcement", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); diff --git a/apps/cli-e2e/src/tests/orgs.e2e.test.ts b/apps/cli-e2e/src/tests/orgs.e2e.test.ts index 028ab99aed..74964819b3 100644 --- a/apps/cli-e2e/src/tests/orgs.e2e.test.ts +++ b/apps/cli-e2e/src/tests/orgs.e2e.test.ts @@ -74,14 +74,14 @@ describe("orgs", () => { describe("orgs:create", () => { testBehaviour.skipIf(isRecording)("creates organization with name", async ({ run }) => { - const result = await run(["orgs", "create", ORG_NAME]); + const result = await run(["orgs", "create", ORG_NAME, "--yes"]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Created organization:"); expect(result.stdout).toMatch(/[a-z]{20}/); }); testBehaviour("exits non-zero without name in non-TTY", async ({ run }) => { - const result = await run(["orgs", "create"]); + const result = await run(["orgs", "create", "--yes"]); expect(result.exitCode).not.toBe(0); }); @@ -94,7 +94,7 @@ describe("orgs", () => { body: { message: "Organization name already in use" }, }), }); - const result = await run(["orgs", "create", ORG_NAME]); + const result = await run(["orgs", "create", ORG_NAME, "--yes"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Organization name already in use"); }); @@ -105,7 +105,7 @@ describe("orgs", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Organization limit reached" } }), }); - const result = await run(["orgs", "create", ORG_NAME]); + const result = await run(["orgs", "create", ORG_NAME, "--yes"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Organization limit reached"); }); @@ -116,7 +116,7 @@ describe("orgs", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 422, body: { message: "Invalid organization name" } }), }); - const result = await run(["orgs", "create", ORG_NAME]); + const result = await run(["orgs", "create", ORG_NAME, "--yes"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid organization name"); }); @@ -127,7 +127,7 @@ describe("orgs", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["orgs", "create", ORG_NAME]); + const result = await run(["orgs", "create", ORG_NAME, "--yes"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -138,7 +138,7 @@ describe("orgs", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["orgs", "create", ORG_NAME]); + const result = await run(["orgs", "create", ORG_NAME, "--yes"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -149,7 +149,7 @@ describe("orgs", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["orgs", "create", ORG_NAME]); + const result = await run(["orgs", "create", ORG_NAME, "--yes"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); diff --git a/apps/cli-e2e/src/tests/projects.e2e.test.ts b/apps/cli-e2e/src/tests/projects.e2e.test.ts index e34dc96926..3dd0f694ae 100644 --- a/apps/cli-e2e/src/tests/projects.e2e.test.ts +++ b/apps/cli-e2e/src/tests/projects.e2e.test.ts @@ -68,7 +68,7 @@ describe("projects", () => { describe("projects:api-keys", () => { testBehaviour("shows default and anon keys", async ({ run, projectRef }) => { - const result = await run(["projects", "api-keys", "--project-ref", projectRef]); + const result = await run(["projects", "api-keys", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("NAME"); expect(result.stdout).toContain("KEY VALUE"); @@ -82,8 +82,7 @@ describe("projects", () => { "api-keys", "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout) as unknown[]; @@ -97,7 +96,7 @@ describe("projects", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["projects", "api-keys", "--project-ref", PROJECT_REF]); + const result = await run(["projects", "api-keys", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); @@ -107,7 +106,7 @@ describe("projects", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["projects", "api-keys", "--project-ref", PROJECT_REF]); + const result = await run(["projects", "api-keys", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); @@ -117,7 +116,7 @@ describe("projects", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), }); - const result = await run(["projects", "api-keys", "--project-ref", PROJECT_REF]); + const result = await run(["projects", "api-keys", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); }); @@ -136,13 +135,14 @@ describe("projects", () => { "us-east-1", "--size", "micro", + "--yes", ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("REFERENCE ID"); }); testBehaviour("exits non-zero without required flags in non-TTY", async ({ run }) => { - const result = await run(["projects", "create", "--org-id", "test-org-id"]); + const result = await run(["projects", "create", "--org-id", "test-org-id", "--yes"]); expect(result.exitCode).not.toBe(0); }); @@ -162,6 +162,7 @@ describe("projects", () => { "password123", "--region", "us-east-1", + "--yes", ]); expect(result.exitCode).not.toBe(0); }); @@ -188,6 +189,7 @@ describe("projects", () => { "password123", "--region", "us-east-1", + "--yes", ]); expect(result.exitCode).not.toBe(0); }); @@ -208,6 +210,7 @@ describe("projects", () => { "password123", "--region", "us-east-1", + "--yes", ]); expect(result.exitCode).not.toBe(0); }); @@ -228,6 +231,7 @@ describe("projects", () => { "password123", "--region", "us-east-1", + "--yes", ]); expect(result.exitCode).not.toBe(0); }); @@ -248,6 +252,7 @@ describe("projects", () => { "password123", "--region", "us-east-1", + "--yes", ]); expect(result.exitCode).not.toBe(0); }); diff --git a/apps/cli-e2e/src/tests/request-log.ts b/apps/cli-e2e/src/tests/request-log.ts new file mode 100644 index 0000000000..4b62c26abb --- /dev/null +++ b/apps/cli-e2e/src/tests/request-log.ts @@ -0,0 +1,12 @@ +export interface RequestEntry { + method: string; + pathname: string; + headers: Record; + body: unknown; +} + +/** The requests the replay server received since the last test cleared its log. */ +export async function getRequestLog(apiUrl: string): Promise { + const res = await fetch(`${apiUrl}/_ctrl/requests`); + return res.json() as Promise; +} diff --git a/apps/cli-e2e/src/tests/secrets.e2e.test.ts b/apps/cli-e2e/src/tests/secrets.e2e.test.ts index 9ace9e4e12..a3d2924351 100644 --- a/apps/cli-e2e/src/tests/secrets.e2e.test.ts +++ b/apps/cli-e2e/src/tests/secrets.e2e.test.ts @@ -7,7 +7,7 @@ import { testBehaviour } from "./test-context.ts"; describe("secrets", () => { describe("secrets:list", () => { testBehaviour("renders fixture data in output", async ({ run, projectRef }) => { - const result = await run(["secrets", "list", "--project-ref", projectRef]); + const result = await run(["secrets", "list", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("NAME"); expect(result.stdout).toContain("DIGEST"); @@ -19,19 +19,12 @@ describe("secrets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["secrets", "list", "--project-ref", PROJECT_REF]); + const result = await run(["secrets", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); testBehaviour("returns json output with --output json", async ({ run, projectRef }) => { - const result = await run([ - "secrets", - "list", - "--output", - "json", - "--project-ref", - projectRef, - ]); + const result = await run(["secrets", "list", "--output", "json", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout) as unknown[]; expect(Array.isArray(parsed)).toBe(true); @@ -43,7 +36,7 @@ describe("secrets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["secrets", "list", "--project-ref", PROJECT_REF]); + const result = await run(["secrets", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); @@ -53,7 +46,7 @@ describe("secrets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), }); - const result = await run(["secrets", "list", "--project-ref", PROJECT_REF]); + const result = await run(["secrets", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); @@ -63,7 +56,7 @@ describe("secrets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["secrets", "list", "--project-ref", PROJECT_REF]); + const result = await run(["secrets", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); @@ -73,19 +66,19 @@ describe("secrets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["secrets", "list", "--project-ref", PROJECT_REF]); + const result = await run(["secrets", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); testBehaviour("includes debug output with --debug", async ({ run, projectRef }) => { - const result = await run(["secrets", "list", "--debug", "--project-ref", projectRef]); + const result = await run(["secrets", "list", "--debug", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); }); }); describe("secrets:set", () => { testBehaviour("sets a single secret", async ({ run, projectRef }) => { - const result = await run(["secrets", "set", "FOO=bar", "--project-ref", projectRef]); + const result = await run(["secrets", "set", "FOO=bar", "--yes", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Finished"); }); @@ -96,8 +89,8 @@ describe("secrets", () => { "set", "FOO=bar", "BAZ=qux", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Finished"); @@ -110,8 +103,8 @@ describe("secrets", () => { "set", "--env-file", ".env.local", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Finished"); @@ -126,7 +119,7 @@ describe("secrets", () => { body: { message: "Validation failed", errors: [{ message: "Invalid secret name" }] }, }), }); - const result = await run(["secrets", "set", "FOO=bar", "--project-ref", PROJECT_REF]); + const result = await run(["secrets", "set", "FOO=bar", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); @@ -136,8 +129,8 @@ describe("secrets", () => { "set", "--env-file", "nonexistent.env", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); }); @@ -148,28 +141,27 @@ describe("secrets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["secrets", "set", "FOO=bar", "--project-ref", PROJECT_REF]); + const result = await run(["secrets", "set", "FOO=bar", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); }); describe("secrets:unset", () => { testBehaviour("removes a secret", async ({ run, projectRef }) => { - const result = await run(["secrets", "unset", "FOO", "--project-ref", projectRef, "--yes"]); + const result = await run(["secrets", "unset", "FOO", "--yes", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Finished"); }); testBehaviour("removes multiple secrets", async ({ run, projectRef }) => { - await run(["secrets", "set", "FOO=bar", "BAR=baz", "--project-ref", projectRef]); + await run(["secrets", "set", "FOO=bar", "BAR=baz", "--yes", `--remote=${projectRef}`]); const result = await run([ "secrets", "unset", "FOO", "BAR", - "--project-ref", - projectRef, "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Finished"); @@ -185,9 +177,8 @@ describe("secrets", () => { "secrets", "unset", "NONEXISTENT", - "--project-ref", - PROJECT_REF, "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); }); @@ -198,7 +189,7 @@ describe("secrets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["secrets", "unset", "FOO", "--project-ref", PROJECT_REF, "--yes"]); + const result = await run(["secrets", "unset", "FOO", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); @@ -208,7 +199,7 @@ describe("secrets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["secrets", "unset", "FOO", "--project-ref", PROJECT_REF, "--yes"]); + const result = await run(["secrets", "unset", "FOO", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); }); diff --git a/apps/cli-e2e/src/tests/sso.e2e.test.ts b/apps/cli-e2e/src/tests/sso.e2e.test.ts index f717699f92..af6a369fc4 100644 --- a/apps/cli-e2e/src/tests/sso.e2e.test.ts +++ b/apps/cli-e2e/src/tests/sso.e2e.test.ts @@ -14,19 +14,19 @@ const MINIMAL_SAML_XML = ` describe("sso", () => { describe("sso:list", () => { testBehaviour("renders fixture data in output", async ({ run, projectRef }) => { - const result = await run(["sso", "list", "--project-ref", projectRef]); + const result = await run(["sso", "list", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("IDENTITY PROVIDER ID"); }); testBehaviour("returns json output with --output json", async ({ run, projectRef }) => { - const result = await run(["sso", "list", "--output", "json", "--project-ref", projectRef]); + const result = await run(["sso", "list", "--output", "json", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(JSON.parse(result.stdout)).toMatchObject({ providers: [] }); }); testBehaviour("includes debug output with --debug", async ({ run, projectRef }) => { - const result = await run(["sso", "list", "--debug", "--project-ref", projectRef]); + const result = await run(["sso", "list", "--debug", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); }); @@ -37,7 +37,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["sso", "list", "--project-ref", PROJECT_REF]); + const result = await run(["sso", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -48,7 +48,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["sso", "list", "--project-ref", PROJECT_REF]); + const result = await run(["sso", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -59,7 +59,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), }); - const result = await run(["sso", "list", "--project-ref", PROJECT_REF]); + const result = await run(["sso", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("SAML 2.0 support is not enabled"); }); @@ -70,7 +70,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["sso", "list", "--project-ref", PROJECT_REF]); + const result = await run(["sso", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -81,7 +81,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["sso", "list", "--project-ref", PROJECT_REF]); + const result = await run(["sso", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); @@ -89,13 +89,13 @@ describe("sso", () => { describe("sso:info", () => { testBehaviour("renders fixture data in output", async ({ run, projectRef }) => { - const result = await run(["sso", "info", "--project-ref", projectRef]); + const result = await run(["sso", "info", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("supabase.co/auth/v1/sso/saml/acs"); }); testBehaviour("returns json output with --output json", async ({ run, projectRef }) => { - const result = await run(["sso", "info", "--output", "json", "--project-ref", projectRef]); + const result = await run(["sso", "info", "--output", "json", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(JSON.parse(result.stdout)).toMatchObject({ acs_url: expect.stringContaining("supabase.co/auth/v1/sso/saml/acs"), @@ -109,7 +109,7 @@ describe("sso", () => { testBehaviour.skipIf(isRecording)( "renders fixture data in output", async ({ run, projectRef }) => { - const result = await run(["sso", "show", PROVIDER_ID, "--project-ref", projectRef]); + const result = await run(["sso", "show", PROVIDER_ID, `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("example.com"); }, @@ -124,8 +124,7 @@ describe("sso", () => { PROVIDER_ID, "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("example.com"); @@ -140,8 +139,7 @@ describe("sso", () => { "show", PROVIDER_ID, "--metadata", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("EntityDescriptor"); @@ -154,7 +152,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["sso", "show", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "show", PROVIDER_ID, `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -165,7 +163,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["sso", "show", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "show", PROVIDER_ID, `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -176,7 +174,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "SSO Identity Provider not found" } }), }); - const result = await run(["sso", "show", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "show", PROVIDER_ID, `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("could not be found"); }); @@ -187,7 +185,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["sso", "show", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "show", PROVIDER_ID, `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -198,7 +196,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["sso", "show", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "show", PROVIDER_ID, `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); @@ -219,8 +217,8 @@ describe("sso", () => { metadataPath, "--domains", "example.com", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("example.com"); @@ -234,8 +232,8 @@ describe("sso", () => { "--metadata-url", "https://example.com/saml/metadata", "--skip-url-validation", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain('"type"'); @@ -255,8 +253,8 @@ describe("sso", () => { "https://example.com/saml/metadata", "--metadata-file", metadataPath, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("metadata"); @@ -271,8 +269,8 @@ describe("sso", () => { "saml", "--metadata-url", "http://localhost:19999/saml.xml", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("HTTPS"); @@ -293,8 +291,8 @@ describe("sso", () => { "saml", "--metadata-file", metadataPath, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -315,8 +313,8 @@ describe("sso", () => { "saml", "--metadata-file", metadataPath, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -340,8 +338,8 @@ describe("sso", () => { "saml", "--metadata-file", metadataPath, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid SAML metadata"); @@ -362,8 +360,8 @@ describe("sso", () => { "saml", "--metadata-file", metadataPath, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -384,8 +382,8 @@ describe("sso", () => { "saml", "--metadata-file", metadataPath, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -402,8 +400,8 @@ describe("sso", () => { PROVIDER_ID, "--add-domains", "example.com", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("example.com"); @@ -419,8 +417,8 @@ describe("sso", () => { PROVIDER_ID, "--domains", "new.com", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("new.com"); @@ -436,8 +434,8 @@ describe("sso", () => { PROVIDER_ID, "--remove-domains", "example.com", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("IDENTITY PROVIDER ID"); @@ -455,8 +453,8 @@ describe("sso", () => { PROVIDER_ID, "--metadata-file", metadataPath, - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("EntityDescriptor"); @@ -472,8 +470,8 @@ describe("sso", () => { "a.com", "--add-domains", "b.com", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("domains"); @@ -491,8 +489,8 @@ describe("sso", () => { PROVIDER_ID, "--add-domains", "example.com", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -510,8 +508,8 @@ describe("sso", () => { PROVIDER_ID, "--add-domains", "example.com", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -529,8 +527,8 @@ describe("sso", () => { PROVIDER_ID, "--add-domains", "example.com", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("could not be found"); @@ -548,8 +546,8 @@ describe("sso", () => { PROVIDER_ID, "--add-domains", "example.com", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -567,8 +565,8 @@ describe("sso", () => { PROVIDER_ID, "--add-domains", "example.com", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -577,7 +575,7 @@ describe("sso", () => { describe("sso:remove", () => { testBehaviour.skipIf(isRecording)("removes a provider", async ({ run }) => { - const result = await run(["sso", "remove", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "remove", PROVIDER_ID, "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("example.com"); }); @@ -588,7 +586,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["sso", "remove", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "remove", PROVIDER_ID, "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -599,7 +597,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["sso", "remove", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "remove", PROVIDER_ID, "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -610,7 +608,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "SSO Identity Provider not found" } }), }); - const result = await run(["sso", "remove", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "remove", PROVIDER_ID, "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("could not be found"); }); @@ -621,7 +619,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["sso", "remove", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "remove", PROVIDER_ID, "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -632,7 +630,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["sso", "remove", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "remove", PROVIDER_ID, "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); diff --git a/apps/cli-e2e/src/tests/stack.e2e.test.ts b/apps/cli-e2e/src/tests/stack.e2e.test.ts index 8993de60eb..d36ea2ae23 100644 --- a/apps/cli-e2e/src/tests/stack.e2e.test.ts +++ b/apps/cli-e2e/src/tests/stack.e2e.test.ts @@ -2,6 +2,7 @@ import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, inject, test } from "vitest"; import { createHarness, exec, type CLIResult } from "@supabase/cli-test-helpers"; +import { getRequestLog } from "./request-log.ts"; import { testBehaviour } from "./test-context.ts"; import { ACCESS_TOKEN } from "./env.ts"; @@ -122,9 +123,7 @@ describe("seed buckets", () => { ); const result = await run(["seed", "buckets"]); expect(result.exitCode).toBe(0); - const requests = await fetch(`${apiUrl}/_ctrl/requests`).then( - (r) => r.json() as Promise>, - ); + const requests = await getRequestLog(apiUrl); expect(requests.some((r) => r.method === "POST" && r.pathname === "/storage/v1/bucket")).toBe( true, ); diff --git a/apps/cli-e2e/src/tests/storage.e2e.test.ts b/apps/cli-e2e/src/tests/storage.e2e.test.ts index b0975ba22d..21f1144fb8 100644 --- a/apps/cli-e2e/src/tests/storage.e2e.test.ts +++ b/apps/cli-e2e/src/tests/storage.e2e.test.ts @@ -1,10 +1,11 @@ import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect } from "vitest"; +import { getRequestLog } from "./request-log.ts"; import { testBehaviour } from "./test-context.ts"; const BUCKET = "cli-e2e-bucket"; -const LOCAL_FLAGS = ["--experimental", "--local"]; +const EXPERIMENTAL_FLAGS = ["--experimental"]; function setupStorageWorkspace(dir: string, relayUrl: string): void { mkdirSync(join(dir, "supabase"), { recursive: true }); @@ -15,18 +16,6 @@ function setupStorageWorkspace(dir: string, relayUrl: string): void { writeFileSync(join(dir, "upload.txt"), "test upload content"); } -interface RequestEntry { - method: string; - pathname: string; - headers: Record; - body: unknown; -} - -async function getRequestLog(apiUrl: string): Promise { - const res = await fetch(`${apiUrl}/_ctrl/requests`); - return res.json() as Promise; -} - async function injectGlobalError(apiUrl: string, status: number, message: string): Promise { await fetch(`${apiUrl}/_ctrl/error-all`, { method: "POST", @@ -42,7 +31,7 @@ async function clearOverrides(apiUrl: string): Promise { describe("storage ls", () => { testBehaviour("lists objects in bucket", async ({ workspace, run, apiUrl }) => { setupStorageWorkspace(workspace.path, apiUrl); - const result = await run(["storage", "ls", ...LOCAL_FLAGS, `ss:///${BUCKET}/`]); + const result = await run(["storage", "ls", ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("hello.txt"); const requests = await getRequestLog(apiUrl); @@ -55,7 +44,7 @@ describe("storage ls", () => { testBehaviour("lists objects recursively", async ({ workspace, run, apiUrl }) => { setupStorageWorkspace(workspace.path, apiUrl); - const result = await run(["storage", "ls", ...LOCAL_FLAGS, "-r", `ss:///${BUCKET}/`]); + const result = await run(["storage", "ls", ...EXPERIMENTAL_FLAGS, "-r", `ss:///${BUCKET}/`]); expect(result.exitCode).toBe(0); const requests = await getRequestLog(apiUrl); expect( @@ -68,7 +57,7 @@ describe("storage ls", () => { testBehaviour("exits 1 on 401", async ({ workspace, run, apiUrl }) => { setupStorageWorkspace(workspace.path, apiUrl); await injectGlobalError(apiUrl, 401, "Invalid token"); - const result = await run(["storage", "ls", ...LOCAL_FLAGS, `ss:///${BUCKET}/`]); + const result = await run(["storage", "ls", ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/`]); expect(result.exitCode).toBe(1); expect(result.stderr).toContain("Invalid token"); await clearOverrides(apiUrl); @@ -77,7 +66,7 @@ describe("storage ls", () => { testBehaviour("exits 1 on 403", async ({ workspace, run, apiUrl }) => { setupStorageWorkspace(workspace.path, apiUrl); await injectGlobalError(apiUrl, 403, "Forbidden"); - const result = await run(["storage", "ls", ...LOCAL_FLAGS, `ss:///${BUCKET}/`]); + const result = await run(["storage", "ls", ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/`]); expect(result.exitCode).toBe(1); expect(result.stderr.length).toBeGreaterThan(0); await clearOverrides(apiUrl); @@ -86,7 +75,7 @@ describe("storage ls", () => { testBehaviour("exits 1 on 429", async ({ workspace, run, apiUrl }) => { setupStorageWorkspace(workspace.path, apiUrl); await injectGlobalError(apiUrl, 429, "Too Many Requests"); - const result = await run(["storage", "ls", ...LOCAL_FLAGS, `ss:///${BUCKET}/`]); + const result = await run(["storage", "ls", ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/`]); expect(result.exitCode).toBe(1); expect(result.stderr.length).toBeGreaterThan(0); await clearOverrides(apiUrl); @@ -95,7 +84,7 @@ describe("storage ls", () => { testBehaviour("exits 1 on 500", async ({ workspace, run, apiUrl }) => { setupStorageWorkspace(workspace.path, apiUrl); await injectGlobalError(apiUrl, 500, "Internal Server Error"); - const result = await run(["storage", "ls", ...LOCAL_FLAGS, `ss:///${BUCKET}/`]); + const result = await run(["storage", "ls", ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/`]); expect(result.exitCode).toBe(1); expect(result.stderr.length).toBeGreaterThan(0); await clearOverrides(apiUrl); @@ -108,7 +97,7 @@ describe("storage cp", () => { const result = await run([ "storage", "cp", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, "upload.txt", `ss:///${BUCKET}/upload.txt`, ]); @@ -126,7 +115,7 @@ describe("storage cp", () => { const result = await run([ "storage", "cp", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, "--cache-control", "no-cache", "upload.txt", @@ -146,7 +135,7 @@ describe("storage cp", () => { const result = await run([ "storage", "cp", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, "--content-type", "application/json", "upload.txt", @@ -166,7 +155,7 @@ describe("storage cp", () => { const result = await run([ "storage", "cp", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, "nonexistent.txt", `ss:///${BUCKET}/x.txt`, ]); @@ -179,7 +168,7 @@ describe("storage cp", () => { const result = await run([ "storage", "cp", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, "upload.txt", `ss:///${BUCKET}/upload.txt`, ]); @@ -194,7 +183,7 @@ describe("storage cp", () => { const result = await run([ "storage", "cp", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, "upload.txt", `ss:///${BUCKET}/upload.txt`, ]); @@ -209,7 +198,7 @@ describe("storage cp", () => { const result = await run([ "storage", "cp", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, "upload.txt", `ss:///${BUCKET}/upload.txt`, ]); @@ -224,7 +213,7 @@ describe("storage cp", () => { const result = await run([ "storage", "cp", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, "upload.txt", `ss:///${BUCKET}/upload.txt`, ]); @@ -238,7 +227,7 @@ describe("storage cp", () => { const result = await run([ "storage", "cp", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/hello.txt`, "hello-download.txt", ]); @@ -250,11 +239,17 @@ describe("storage mv", () => { testBehaviour("moves file within bucket", async ({ workspace, run, apiUrl }) => { setupStorageWorkspace(workspace.path, apiUrl); // Upload source file so the move has something to move in staging. - await run(["storage", "cp", ...LOCAL_FLAGS, "upload.txt", `ss:///${BUCKET}/mv-source.txt`]); + await run([ + "storage", + "cp", + ...EXPERIMENTAL_FLAGS, + "upload.txt", + `ss:///${BUCKET}/mv-source.txt`, + ]); const result = await run([ "storage", "mv", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/mv-source.txt`, `ss:///${BUCKET}/mv-dest.txt`, ]); @@ -277,7 +272,7 @@ describe("storage mv", () => { const result = await run([ "storage", "mv", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/a.txt`, `ss:///${BUCKET}/b.txt`, ]); @@ -292,7 +287,7 @@ describe("storage mv", () => { const result = await run([ "storage", "mv", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/a.txt`, `ss:///${BUCKET}/b.txt`, ]); @@ -307,7 +302,7 @@ describe("storage mv", () => { const result = await run([ "storage", "mv", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/a.txt`, `ss:///${BUCKET}/b.txt`, ]); @@ -322,7 +317,7 @@ describe("storage mv", () => { const result = await run([ "storage", "mv", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/a.txt`, `ss:///${BUCKET}/b.txt`, ]); @@ -336,12 +331,18 @@ describe("storage rm", () => { testBehaviour("removes a file from storage", async ({ workspace, run, apiUrl }) => { setupStorageWorkspace(workspace.path, apiUrl); // Upload the file to remove. - await run(["storage", "cp", ...LOCAL_FLAGS, "upload.txt", `ss:///${BUCKET}/rm-target.txt`]); + await run([ + "storage", + "cp", + ...EXPERIMENTAL_FLAGS, + "upload.txt", + `ss:///${BUCKET}/rm-target.txt`, + ]); const result = await run([ "storage", "rm", "--yes", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/rm-target.txt`, ]); expect(result.exitCode).toBe(0); @@ -356,13 +357,13 @@ describe("storage rm", () => { testBehaviour("removes multiple files", async ({ workspace, run, apiUrl }) => { setupStorageWorkspace(workspace.path, apiUrl); writeFileSync(join(workspace.path, "file2.txt"), "second file"); - await run(["storage", "cp", ...LOCAL_FLAGS, "upload.txt", `ss:///${BUCKET}/rm-a.txt`]); - await run(["storage", "cp", ...LOCAL_FLAGS, "file2.txt", `ss:///${BUCKET}/rm-b.txt`]); + await run(["storage", "cp", ...EXPERIMENTAL_FLAGS, "upload.txt", `ss:///${BUCKET}/rm-a.txt`]); + await run(["storage", "cp", ...EXPERIMENTAL_FLAGS, "file2.txt", `ss:///${BUCKET}/rm-b.txt`]); const result = await run([ "storage", "rm", "--yes", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/rm-a.txt`, `ss:///${BUCKET}/rm-b.txt`, ]); @@ -382,7 +383,7 @@ describe("storage rm", () => { "storage", "rm", "--yes", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/file.txt`, ]); expect(result.exitCode).toBe(1); @@ -397,7 +398,7 @@ describe("storage rm", () => { "storage", "rm", "--yes", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/file.txt`, ]); expect(result.exitCode).toBe(1); @@ -412,7 +413,7 @@ describe("storage rm", () => { "storage", "rm", "--yes", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/file.txt`, ]); expect(result.exitCode).toBe(1); @@ -427,7 +428,7 @@ describe("storage rm", () => { "storage", "rm", "--yes", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/file.txt`, ]); expect(result.exitCode).toBe(1); diff --git a/apps/cli-e2e/src/tests/target-selection.e2e.test.ts b/apps/cli-e2e/src/tests/target-selection.e2e.test.ts new file mode 100644 index 0000000000..183a5f691d --- /dev/null +++ b/apps/cli-e2e/src/tests/target-selection.e2e.test.ts @@ -0,0 +1,29 @@ +import { describe, expect } from "vitest"; +import { linkProject } from "./link-project.ts"; +import { getRequestLog } from "./request-log.ts"; +import { testBehaviour } from "./test-context.ts"; + +const DEAD_DB_URL = "postgresql://postgres:postgres@localhost:1/postgres"; + +describe("remote target selection", () => { + testBehaviour( + "reads a space-separated --remote as --remote=", + async ({ run, projectRef, workspace, apiUrl }) => { + linkProject(workspace.path, projectRef); + const result = await run(["db", "advisors", "--type", "security", "--remote", projectRef]); + expect(result.exitCode).toBe(0); + const requests = await getRequestLog(apiUrl); + expect(requests.map((r) => `${r.method} ${r.pathname}`)).toContain( + `GET /v1/projects/${projectRef}/advisors/security`, + ); + }, + ); + + testBehaviour("rejects --remote= on a hosted command", async ({ run, apiUrl }) => { + const result = await run(["secrets", "list", `--remote=${DEAD_DB_URL}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("--remote takes a project ref or a branch name."); + const requests = await getRequestLog(apiUrl); + expect(requests.filter((r) => r.pathname.endsWith("/secrets"))).toEqual([]); + }); +}); diff --git a/apps/cli-go/.gitignore b/apps/cli-go/.gitignore deleted file mode 100644 index 32520228b6..0000000000 --- a/apps/cli-go/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -# General -.DS_Store -.env - -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib -/cli -/main -/supabase-go - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -# IDE -/.vscode -/.idea - -# NPM -node_modules -package-lock.json - -# Initialized by cli for local testing -/supabase diff --git a/apps/cli-go/.golangci.yml b/apps/cli-go/.golangci.yml deleted file mode 100644 index 711143ee31..0000000000 --- a/apps/cli-go/.golangci.yml +++ /dev/null @@ -1,49 +0,0 @@ -version: "2" -linters: - enable: - - dogsled - - dupl - - gosec - - misspell - - nakedret - - staticcheck - - unconvert - - unparam - - whitespace - settings: - dupl: - threshold: 250 - exclusions: - generated: lax - rules: - - text: "ST1003:" - linters: - - staticcheck - - path: _test\.go - linters: - - gosec - text: G101 - - path: internal/utils/docker\.go - linters: - - gosec - text: G117 - - presets: - - comments - - common-false-positives - - legacy - - std-error-handling - paths: - - third_party$ - - builtin$ - - examples$ -formatters: - enable: - - gofmt - - goimports - exclusions: - generated: lax - paths: - - third_party$ - - builtin$ - - examples$ diff --git a/apps/cli-go/CONTRIBUTING.md b/apps/cli-go/CONTRIBUTING.md deleted file mode 100644 index c53f8d6c91..0000000000 --- a/apps/cli-go/CONTRIBUTING.md +++ /dev/null @@ -1,50 +0,0 @@ -# Welcome to Supabase CLI contributing guide - -## Release process - -We release to stable channel every two weeks via `main` branch. - -We release to beta channel on merge to `develop` branch. - -Hotfixes are released manually. Follow these steps: - -1. Create a new branch named `N.N.x` from latest stable version. For eg. - 1. If stable is on `v1.2.3` and beta is on `v1.3.6`, create `1.2.x` branch. - 2. If stable is on `v1.3.1` and beta is on `v1.3.6`, create `1.3.x` branch (or simply release all patch versions). -2. Cherry-pick your hotfix on top of `N.N.x` branch. -3. Run the [Release (Beta)](https://github.com/supabase/cli/actions/workflows/release-beta.yml) workflow targetting `N.N.x` branch. -4. Verify your hotfix locally with `npx supabase@N.N.x help` -5. Edit [GitHub releases](https://github.com/supabase/cli/releases) to set your hotfix pre-release as latest stable. - -After promoting the next beta version to stable, previous `N.N.x` branches may be deleted. - -To revert a stable release, set a previous release to latest. This will update brew and scoop to an old version. There's no need to revert npm as it supports version pinning. - -## Unit testing - -All new code should aim to improve [test coverage](https://coveralls.io/github/supabase/cli). - -We use mock objects for unit testing code that interacts with external systems, such as - -- local filesystem (via [afero](https://github.com/spf13/afero)) -- Postgres database (via [pgmock](https://github.com/jackc/pgmock)) -- Supabase API (via [gock](https://github.com/h2non/gock)) - -Wrappers and test helper methods can be found under [internal/testing](internal/testing). - -Integration tests are created under [test](test). To run all tests: - -```bash -go test ./... -race -v -count=1 -failfast -``` - -## API client - -The Supabase API client is generated from OpenAPI spec. See [our guide](api/README.md) for updating the client and types. - -## Testing local pg-delta builds - -The Go binary no longer runs pg-delta. The TypeScript CLI bundles -`@supabase/pg-delta` in-process. To test a local pg-delta build, update the -`@supabase/pg-delta` dependency pin in `apps/cli/package.json` / -`pnpm-workspace.yaml`. diff --git a/apps/cli-go/api/README.md b/apps/cli-go/api/README.md deleted file mode 100644 index 326d08315b..0000000000 --- a/apps/cli-go/api/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# Supabase OpenAPI Specification - -This directory contains the OpenAPI specification for Supabase Management APIs. - -`v1-openapi.yaml` is a committed snapshot of the upstream specification and is the input -`go generate` uses to build the Go [client](../pkg/api/client.gen.go) and -[types](../pkg/api/types.gen.go). `overlay.yaml` patches the specification on its way -through codegen, for shapes `oapi-codegen` cannot consume directly. - -Committing the snapshot keeps codegen reproducible: `go generate` needs no network -access, and CI's `Codegen` check fails only when a pull request leaves `pkg/api` out of -sync with the snapshot. Upstream changes arrive through the -[API Sync workflow](../../../.github/workflows/cli-go-api-sync.yml), which refreshes the -snapshot and regenerates the client together in one pull request, so a spec change no -longer breaks unrelated pull requests. - -## Updating the specification - -The specification is generated from our NestJS middleware. `v1-openapi.yaml` is -refreshed from **staging** (`https://api.supabase.green/api/v1-yaml`) by the API -Sync workflow, so staging is the snapshot's authoritative upstream and the Go -client tracks the API as staging exposes it. The production release is viewable -as [Swagger UI](https://api.supabase.com/api/v1), which is useful for reading the -shipped API but is not what the snapshot is generated from -- expect it to lag -the snapshot. - -Refreshing the snapshot by hand is rarely necessary, since API Sync does it -automatically. To pick up a change from local development before it reaches staging: - -1. Update the snapshot - -```bash -curl -fsSL -o api/v1-openapi.yaml http://127.0.0.1:8080/api/v1-yaml -``` - -2. Regenerate the Go client and API types - -```bash -go generate -``` - -3. [Optional] Add [properties](https://swagger.io/docs/specification/basic-structure/) that NestJS does not generate to `overlay.yaml` diff --git a/apps/cli-go/api/overlay.yaml b/apps/cli-go/api/overlay.yaml deleted file mode 100644 index 025cca4465..0000000000 --- a/apps/cli-go/api/overlay.yaml +++ /dev/null @@ -1,79 +0,0 @@ -overlay: 1.0.0 -info: - title: Overlay - version: 0.0.0 -actions: -- target: $.components.schemas.*.properties.password_required_characters.enum - description: Converts enum with special characters to a string - remove: true -- target: $.components.schemas.*.properties.password_required_characters - description: Optionally adds back the enum with escaped backslash and quotes - update: - enum: - - abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:0123456789 - - abcdefghijklmnopqrstuvwxyz:ABCDEFGHIJKLMNOPQRSTUVWXYZ:0123456789 - - abcdefghijklmnopqrstuvwxyz:ABCDEFGHIJKLMNOPQRSTUVWXYZ:0123456789:!@#$%^&*()_+-=[]{};'\\\\:\"|<>?,./`~ - - '' -- target: $.components.schemas.*.properties.attribute_mapping.properties.keys.additionalProperties.properties.default.anyOf - description: Replaces request union type with interface for easier casting - remove: true -- target: $.components.schemas.*.properties.saml.properties.attribute_mapping.properties.keys.additionalProperties.properties.default.anyOf - description: Replaces response union type with interface for easier casting - remove: true -- target: $.components.schemas.*.properties.items.items.properties.saml.properties.attribute_mapping.properties.keys.additionalProperties.properties.default.anyOf - description: Replaces list union type with interface for easier casting - remove: true -- target: $.components.schemas.*.properties.saml.properties.name_id_format.enum - description: Converts enum with duplicated type to a string - remove: true -- target: $.components.schemas.*.properties.items.items.properties.saml.properties.name_id_format.enum - description: Converts list of enum with duplicated type to a string - remove: true -- target: $.components.schemas.*.properties.connectionString - description: Removes deprecated field that conflicts with naming convention - remove: true -- target: $.components.schemas.V1CreateProjectBody.properties.release_channel - description: Removes deprecated null-only field that oapi-codegen cannot map - remove: true -- target: $.components.schemas.V1CreateProjectBody.properties.postgres_engine - description: Removes deprecated null-only field that oapi-codegen cannot map - remove: true -- target: $.components.schemas.JitListAccessResponse_Output.properties.items.items.anyOf[0].properties.invite_id - description: Replaces null-only project user invite id with nullable UUID for oapi-codegen - update: - type: string - format: uuid - nullable: true -- target: $.components.schemas.JitListAccessResponse_Output.properties.items.items.anyOf[0].properties.expires_at - description: Replaces null-only project user invite expiry with nullable string for oapi-codegen - update: - type: string - nullable: true -- target: $.components.schemas.JitListAccessResponse_Output.properties.items.items.anyOf[1].properties.user_id - description: Replaces null-only invited user id with nullable UUID for oapi-codegen - update: - type: string - format: uuid - nullable: true -- target: $.paths.*.*.parameters[?(@.name=='branch_id_or_ref')] - update: - schema: - type: string -- target: $.paths.*.*.parameters[?(@.name=='services')].schema.anyOf - description: Removes comma-string-or-array union so the array-of-enum shape can be set directly - remove: true -- target: $.paths.*.*.parameters[?(@.name=='services')].schema - description: Replaces comma-string-or-array union with a plain array of enums for oapi-codegen - update: - type: array - items: - type: string - enum: - - auth - - db - - db_postgres_user - - pooler - - realtime - - rest - - storage - - pg_bouncer diff --git a/apps/cli-go/api/v1-openapi.yaml b/apps/cli-go/api/v1-openapi.yaml deleted file mode 100644 index faf1b4fe31..0000000000 --- a/apps/cli-go/api/v1-openapi.yaml +++ /dev/null @@ -1,16883 +0,0 @@ -openapi: 3.0.0 -paths: - /v1/branches/{branch_id_or_ref}: - get: - description: Fetches configurations of the specified database branch - operationId: v1-get-a-branch-config - parameters: - - name: branch_id_or_ref - required: true - in: path - description: Branch ref or deprecated branch ID - schema: - example: abcdefghijklmnopqrst - anyOf: - - type: string - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - description: Project ref - example: abcdefghijklmnopqrst - - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - deprecated: true - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/BranchDetailResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve database branch - security: - - bearer: [] - summary: Get database branch config - tags: - - Environments - x-badges: - - name: 'OAuth scope: environment:read' - position: after - x-endpoint-owners: - - dev-workflows - x-fga-permissions: - - - branching_development_read - - - branching_production_read - x-oauth-scope: environment:read - patch: - description: Updates the configuration of the specified database branch - operationId: v1-update-a-branch-config - parameters: - - name: branch_id_or_ref - required: true - in: path - description: Branch ref or deprecated branch ID - schema: - example: abcdefghijklmnopqrst - anyOf: - - type: string - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - description: Project ref - example: abcdefghijklmnopqrst - - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - deprecated: true - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateBranchBody' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/BranchResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to update database branch - security: - - bearer: [] - summary: Update database branch config - tags: - - Environments - x-badges: - - name: 'OAuth scope: environment:write' - position: after - x-endpoint-owners: - - dev-workflows - x-fga-permissions: - - - branching_development_write - - - branching_production_write - x-oauth-scope: environment:write - delete: - description: >- - Deletes the specified database branch. By default, deletes immediately. - Use force=false to schedule deletion with 1-hour grace period (only when - soft deletion is enabled). - operationId: v1-delete-a-branch - parameters: - - name: branch_id_or_ref - required: true - in: path - description: Branch ref or deprecated branch ID - schema: - example: abcdefghijklmnopqrst - anyOf: - - type: string - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - description: Project ref - example: abcdefghijklmnopqrst - - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - deprecated: true - - name: force - required: false - in: query - description: >- - If set to false, schedule deletion with 1-hour grace period (only - when soft deletion is enabled). - schema: - example: false - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/BranchDeleteResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to delete database branch - security: - - bearer: [] - summary: Delete a database branch - tags: - - Environments - x-badges: - - name: 'OAuth scope: environment:write' - position: after - x-endpoint-owners: - - dev-workflows - x-fga-permissions: - - - branching_development_delete - - - branching_production_delete - x-oauth-scope: environment:write - /v1/branches/{branch_id_or_ref}/push: - post: - description: Pushes the specified database branch - operationId: v1-push-a-branch - parameters: - - name: branch_id_or_ref - required: true - in: path - description: Branch ref or deprecated branch ID - schema: - example: abcdefghijklmnopqrst - anyOf: - - type: string - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - description: Project ref - example: abcdefghijklmnopqrst - - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - deprecated: true - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/BranchActionBody' - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/BranchUpdateResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to push database branch - security: - - bearer: [] - summary: Pushes a database branch - tags: - - Environments - x-badges: - - name: 'OAuth scope: environment:write' - position: after - x-endpoint-owners: - - dev-workflows - x-fga-permissions: - - - branching_development_write - - - branching_production_write - x-oauth-scope: environment:write - /v1/branches/{branch_id_or_ref}/merge: - post: - description: Merges the specified database branch - operationId: v1-merge-a-branch - parameters: - - name: branch_id_or_ref - required: true - in: path - description: Branch ref or deprecated branch ID - schema: - example: abcdefghijklmnopqrst - anyOf: - - type: string - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - description: Project ref - example: abcdefghijklmnopqrst - - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - deprecated: true - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/BranchActionBody' - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/BranchUpdateResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to merge database branch - security: - - bearer: [] - summary: Merges a database branch - tags: - - Environments - x-badges: - - name: 'OAuth scope: environment:write' - position: after - x-endpoint-owners: - - dev-workflows - x-fga-permissions: - - - branching_development_write - - - branching_production_write - x-oauth-scope: environment:write - /v1/branches/{branch_id_or_ref}/reset: - post: - description: Resets the specified database branch - operationId: v1-reset-a-branch - parameters: - - name: branch_id_or_ref - required: true - in: path - description: Branch ref or deprecated branch ID - schema: - example: abcdefghijklmnopqrst - anyOf: - - type: string - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - description: Project ref - example: abcdefghijklmnopqrst - - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - deprecated: true - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/BranchActionBody' - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/BranchUpdateResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to reset database branch - security: - - bearer: [] - summary: Resets a database branch - tags: - - Environments - x-badges: - - name: 'OAuth scope: environment:write' - position: after - x-endpoint-owners: - - dev-workflows - x-fga-permissions: - - - branching_development_write - - - branching_production_write - x-oauth-scope: environment:write - /v1/branches/{branch_id_or_ref}/restore: - post: - description: Cancels scheduled deletion and restores the branch to active state - operationId: v1-restore-a-branch - parameters: - - name: branch_id_or_ref - required: true - in: path - description: Branch ref or deprecated branch ID - schema: - example: abcdefghijklmnopqrst - anyOf: - - type: string - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - description: Project ref - example: abcdefghijklmnopqrst - - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - deprecated: true - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/BranchRestoreResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to restore database branch - security: - - bearer: [] - summary: Restore a scheduled branch deletion - tags: - - Environments - x-badges: - - name: 'OAuth scope: environment:write' - position: after - x-endpoint-owners: - - dev-workflows - x-fga-permissions: - - - branching_development_write - - - branching_production_write - x-oauth-scope: environment:write - /v1/branches/{branch_id_or_ref}/diff: - get: - description: |- - Diffs the specified database branch - - This endpoint is currently in its **Beta** stage. - operationId: v1-diff-a-branch - parameters: - - name: branch_id_or_ref - required: true - in: path - description: Branch ref or deprecated branch ID - schema: - example: abcdefghijklmnopqrst - anyOf: - - type: string - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - description: Project ref - example: abcdefghijklmnopqrst - - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - deprecated: true - - name: included_schemas - required: false - in: query - schema: - example: public,auth - type: string - - name: pgdelta - required: false - in: query - description: |- - Use pg-delta instead of Migra for diffing when true. - Boolean string. - - Truthy values: `true`, `1`, `yes`, `on`, `y`, `enabled` - - Falsy values: `false`, `0`, `no`, `off`, `n`, `disabled` - schema: - example: 'true' - type: string - responses: - '200': - content: - text/plain: - schema: - type: string - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to diff database branch - security: - - bearer: [] - summary: Diffs a database branch - tags: - - Environments - x-badges: - - name: 'OAuth scope: environment:write' - position: after - - name: Beta - position: before - x-endpoint-owners: - - dev-workflows - x-fga-permissions: - - - branching_development_write - - - branching_production_write - x-oauth-scope: environment:write - x-scalar-stability: experimental - /v1/projects: - get: - description: Returns a list of all projects you've previously created. - operationId: v1-list-all-projects - parameters: [] - responses: - '200': - description: '' - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/V1ProjectWithDatabaseResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: List all projects - tags: - - Projects - x-badges: - - name: 'OAuth scope: projects:read' - position: after - x-endpoint-owners: - - control-plane - x-fga-permissions: - - - projects_read - x-oauth-scope: projects:read - post: - operationId: v1-create-a-project - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/V1CreateProjectBody' - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/V1ProjectResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Create a project - tags: - - Projects - x-badges: - - name: 'OAuth scope: projects:write' - position: after - x-endpoint-owners: - - control-plane - - infra - x-fga-permissions: - - - organization_projects_create - x-oauth-scope: projects:write - /v1/projects/available-regions: - get: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-get-available-regions - parameters: - - name: organization_slug - required: true - in: query - description: Slug of your organization - schema: - example: tsrqponmlkjihgfedcba - type: string - - name: continent - required: false - in: query - description: >- - Continent code to determine regional recommendations: NA (North - America), SA (South America), EU (Europe), AF (Africa), AS (Asia), - OC (Oceania), AN (Antarctica) - schema: - example: NA - type: string - enum: - - NA - - SA - - EU - - AF - - AS - - OC - - AN - - name: desired_instance_size - required: false - in: query - description: >- - Desired instance size. Omit this field to always default to the - smallest possible size. - schema: - type: string - enum: - - nano - - micro - - small - - medium - - large - - xlarge - - 2xlarge - - 4xlarge - - 8xlarge - - 12xlarge - - 16xlarge - - 24xlarge - - 24xlarge_optimized_memory - - 24xlarge_optimized_cpu - - 24xlarge_high_memory - - 48xlarge - - 48xlarge_optimized_memory - - 48xlarge_optimized_cpu - - 48xlarge_high_memory - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/RegionsInfoV1_Output' - security: - - bearer: [] - summary: Gets the list of available regions that can be used for a new project - tags: - - Projects - x-badges: - - name: 'OAuth scope: organizations:read' - position: after - - name: Beta - position: before - x-endpoint-owners: - - infra - x-oauth-scope: organizations:read - x-scalar-stability: experimental - /v1/organizations: - get: - description: Returns a list of organizations that you currently belong to. - operationId: v1-list-all-organizations - parameters: [] - responses: - '200': - description: '' - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/OrganizationResponseV1_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Unexpected error listing organizations - security: - - bearer: [] - summary: List all organizations - tags: - - Organizations - x-badges: - - name: 'OAuth scope: organizations:read' - position: after - x-endpoint-owners: - - control-plane - x-fga-permissions: - - - organizations_read - x-oauth-scope: organizations:read - post: - operationId: v1-create-an-organization - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateOrganizationV1' - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/OrganizationResponseV1_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Unexpected error creating an organization - security: - - bearer: [] - summary: Create an organization - tags: - - Organizations - x-endpoint-owners: - - control-plane - - billing - x-fga-permissions: - - - organizations_create - /v1/oauth/authorize: - get: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-authorize-user - parameters: - - name: client_id - required: true - in: query - schema: - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - example: 66666666-6666-4666-8666-666666666666 - type: string - - name: response_type - required: true - in: query - schema: - example: code - type: string - enum: - - code - - token - - id_token token - - name: redirect_uri - required: true - in: query - schema: - example: https://app.acme.com/auth/callback - type: string - - name: scope - required: false - in: query - schema: - example: projects:read projects:write - type: string - - name: state - required: false - in: query - schema: - example: st_9f4d3a206b2e4a7e8c91 - type: string - - name: response_mode - required: false - in: query - schema: - example: query - type: string - - name: code_challenge - required: false - in: query - schema: - example: Z_P4EKbGwIkA01e3Y5fp4tMCvn_Ae5nUw7qY7XwkTrQ - type: string - - name: code_challenge_method - required: false - in: query - schema: - example: S256 - type: string - enum: - - plain - - sha256 - - S256 - - name: organization_slug - required: false - in: query - description: Organization slug - schema: - pattern: ^[\w-]+$ - example: tsrqponmlkjihgfedcba - type: string - - name: target_flow - required: false - in: query - schema: - type: string - - name: resource - required: false - in: query - description: Resource indicator for MCP (Model Context Protocol) clients - schema: - format: uri - example: https://mcp.supabase.com/projects - type: string - responses: - '204': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - summary: Authorize user through oauth - tags: - - OAuth - x-badges: - - name: Beta - position: before - x-endpoint-owners: - - auth - - control-plane - x-scalar-stability: experimental - /v1/oauth/token: - post: - description: >- - Supports `authorization_code`, `refresh_token`, and - `urn:ietf:params:oauth:grant-type:jwt-bearer` grant types. The - `jwt-bearer` grant type (IDJAG — identity-directed JWT assertion) is in - beta and available on Team and Enterprise plans only. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-exchange-oauth-token - parameters: [] - requestBody: - required: true - content: - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/OAuthTokenBody' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/OAuthTokenResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - summary: Exchange auth code for user's access and refresh token - tags: - - OAuth - x-badges: - - name: Beta - position: before - x-endpoint-owners: - - auth - - control-plane - x-scalar-stability: experimental - /v1/oauth/revoke: - post: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-revoke-token - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/OAuthRevokeTokenBody' - responses: - '204': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - summary: Revoke oauth app authorization and it's corresponding tokens - tags: - - OAuth - x-badges: - - name: Beta - position: before - x-endpoint-owners: - - auth - - control-plane - x-scalar-stability: experimental - /v1/oauth/authorize/project-claim: - get: - description: >- - Initiates the OAuth authorization flow for the specified provider. After - successful authentication, the user can claim ownership of the specified - project. - operationId: v1-oauth-authorize-project-claim - parameters: - - name: project_ref - required: true - in: query - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: client_id - required: true - in: query - schema: - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - example: 66666666-6666-4666-8666-666666666666 - type: string - - name: response_type - required: true - in: query - schema: - example: code - type: string - enum: - - code - - token - - id_token token - - name: redirect_uri - required: true - in: query - schema: - example: https://app.acme.com/auth/callback - type: string - - name: state - required: false - in: query - schema: - example: st_9f4d3a206b2e4a7e8c91 - type: string - - name: response_mode - required: false - in: query - schema: - example: query - type: string - - name: code_challenge - required: false - in: query - schema: - example: Z_P4EKbGwIkA01e3Y5fp4tMCvn_Ae5nUw7qY7XwkTrQ - type: string - - name: code_challenge_method - required: false - in: query - schema: - example: S256 - type: string - enum: - - plain - - sha256 - - S256 - responses: - '204': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Authorize user through oauth and claim a project - tags: - - OAuth - x-endpoint-owners: - - control-plane - x-fga-permissions: - - - organization_admin_write - - project_admin_write - /v1/snippets: - get: - operationId: v1-list-all-snippets - parameters: - - name: project_ref - required: false - in: query - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: cursor - required: false - in: query - schema: - type: string - - name: limit - required: false - in: query - schema: - type: string - minimum: 1 - maximum: 100 - - name: sort_by - required: false - in: query - schema: - enum: - - name - - inserted_at - type: string - - name: sort_order - required: false - in: query - schema: - enum: - - asc - - desc - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/SnippetList_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to list user's SQL snippets - security: - - bearer: [] - summary: Lists SQL snippets for the logged in user - tags: - - Database - x-badges: - - name: 'OAuth scope: database:read' - position: after - x-endpoint-owners: - - control-plane - x-fga-permissions: - - - snippets_read - x-oauth-scope: database:read - /v1/snippets/{id}: - get: - operationId: v1-get-a-snippet - parameters: - - name: id - required: true - in: path - schema: - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - example: 44444444-4444-4444-8444-444444444444 - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/SnippetResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve SQL snippet - security: - - bearer: [] - summary: Gets a specific SQL snippet - tags: - - Database - x-badges: - - name: 'OAuth scope: database:read' - position: after - x-endpoint-owners: - - control-plane - x-fga-permissions: - - - snippets_read - x-oauth-scope: database:read - /v1/profile: - get: - operationId: v1-get-profile - parameters: [] - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/V1ProfileResponse_Output' - security: - - bearer: [] - summary: Gets the user's profile - tags: - - Profile - x-endpoint-owners: - - control-plane - /v1/projects/{ref}/actions: - head: - description: Returns the total number of action runs of the specified project. - operationId: v1-count-action-runs - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - headers: - X-Total-Count: - schema: - type: integer - format: int64 - minimum: 0 - description: total count value - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to count action runs - security: - - bearer: [] - summary: Count the number of action runs - tags: - - Environments - x-badges: - - name: 'OAuth scope: environment:read' - position: after - x-fga-permissions: - - - action_runs_read - x-oauth-scope: environment:read - get: - description: Returns a paginated list of action runs of the specified project. - operationId: v1-list-action-runs - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: offset - required: false - in: query - schema: - minimum: 0 - example: 0 - type: number - - name: limit - required: false - in: query - schema: - minimum: 10 - example: 20 - type: number - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ListActionRunResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to list action runs - security: - - bearer: [] - summary: List all action runs - tags: - - Environments - x-badges: - - name: 'OAuth scope: environment:read' - position: after - x-endpoint-owners: - - dev-workflows - x-fga-permissions: - - - action_runs_read - x-oauth-scope: environment:read - /v1/projects/{ref}/actions/{run_id}: - get: - description: Returns the current status of the specified action run. - operationId: v1-get-action-run - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: run_id - required: true - in: path - description: Action Run ID - schema: - example: run_01hq3q9m7y5q7e4a7x2c8m1p4n - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ActionRunResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to get action run status - security: - - bearer: [] - summary: Get the status of an action run - tags: - - Environments - x-badges: - - name: 'OAuth scope: environment:read' - position: after - x-endpoint-owners: - - dev-workflows - x-fga-permissions: - - - action_runs_read - x-oauth-scope: environment:read - /v1/projects/{ref}/actions/{run_id}/status: - patch: - description: Updates the status of an ongoing action run. - operationId: v1-update-action-run-status - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: run_id - required: true - in: path - description: Action Run ID - schema: - example: run_01hq3q9m7y5q7e4a7x2c8m1p4n - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateRunStatusBody' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateRunStatusResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to update action run status - security: - - bearer: [] - summary: Update the status of an action run - tags: - - Environments - x-badges: - - name: 'OAuth scope: environment:write' - position: after - x-endpoint-owners: - - dev-workflows - x-fga-permissions: - - - action_runs_write - x-oauth-scope: environment:write - /v1/projects/{ref}/actions/{run_id}/logs: - get: - description: Returns the logs from the specified action run. - operationId: v1-get-action-run-logs - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: run_id - required: true - in: path - description: Action Run ID - schema: - example: run_01hq3q9m7y5q7e4a7x2c8m1p4n - type: string - responses: - '200': - content: - text/plain: - schema: - type: string - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to get action run logs - security: - - bearer: [] - summary: Get the logs of an action run - tags: - - Environments - x-badges: - - name: 'OAuth scope: environment:read' - position: after - x-endpoint-owners: - - dev-workflows - x-fga-permissions: - - - action_runs_read - x-oauth-scope: environment:read - /v1/projects/{ref}/api-keys: - get: - operationId: v1-get-project-api-keys - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: reveal - required: false - in: query - description: |- - Boolean string. - - Truthy values: `true`, `1`, `yes`, `on`, `y`, `enabled` - - Falsy values: `false`, `0`, `no`, `off`, `n`, `disabled` - schema: - example: 'true' - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/ApiKeyResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Get project api keys - tags: - - Secrets - x-badges: - - name: 'OAuth scope: secrets:read' - position: after - x-endpoint-owners: - - auth - - control-plane - x-fga-permissions: - - - api_gateway_keys_read - - - api_gateway_keys_read - - api_gateway_keys_secret_read - x-oauth-scope: secrets:read - post: - operationId: v1-create-project-api-key - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: reveal - required: false - in: query - description: |- - Boolean string. - - Truthy values: `true`, `1`, `yes`, `on`, `y`, `enabled` - - Falsy values: `false`, `0`, `no`, `off`, `n`, `disabled` - schema: - example: 'true' - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateApiKeyBody' - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ApiKeyResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Creates a new API key for the project - tags: - - Secrets - x-badges: - - name: 'OAuth scope: secrets:write' - position: after - x-endpoint-owners: - - auth - - control-plane - x-fga-permissions: - - - api_gateway_keys_write - x-oauth-scope: secrets:write - /v1/projects/{ref}/api-keys/legacy: - get: - operationId: v1-get-project-legacy-api-keys - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/LegacyApiKeysResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: >- - Check whether JWT based legacy (anon, service_role) API keys are - enabled. This API endpoint will be removed in the future, check for HTTP - 404 Not Found. - tags: - - Secrets - x-badges: - - name: 'OAuth scope: secrets:read' - position: after - x-endpoint-owners: - - auth - - control-plane - x-fga-permissions: - - - api_gateway_keys_read - x-oauth-scope: secrets:read - put: - operationId: v1-update-project-legacy-api-keys - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: enabled - required: true - in: query - description: |- - Boolean string. - - Truthy values: `true`, `1`, `yes`, `on`, `y`, `enabled` - - Falsy values: `false`, `0`, `no`, `off`, `n`, `disabled` - schema: - example: 'true' - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/LegacyApiKeysResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: >- - Disable or re-enable JWT based legacy (anon, service_role) API keys. - This API endpoint will be removed in the future, check for HTTP 404 Not - Found. - tags: - - Secrets - x-badges: - - name: 'OAuth scope: secrets:write' - position: after - x-endpoint-owners: - - auth - - control-plane - x-fga-permissions: - - - api_gateway_keys_write - x-oauth-scope: secrets:write - /v1/projects/{ref}/api-keys/{id}: - patch: - operationId: v1-update-project-api-key - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: id - required: true - in: path - schema: - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - example: 22222222-2222-4222-8222-222222222222 - type: string - - name: reveal - required: false - in: query - description: |- - Boolean string. - - Truthy values: `true`, `1`, `yes`, `on`, `y`, `enabled` - - Falsy values: `false`, `0`, `no`, `off`, `n`, `disabled` - schema: - example: 'true' - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateApiKeyBody' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ApiKeyResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '404': - description: API key not found for project - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Updates an API key for the project - tags: - - Secrets - x-badges: - - name: 'OAuth scope: secrets:write' - position: after - x-endpoint-owners: - - auth - - control-plane - x-fga-permissions: - - - api_gateway_keys_write - x-oauth-scope: secrets:write - get: - operationId: v1-get-project-api-key - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: id - required: true - in: path - schema: - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - example: 22222222-2222-4222-8222-222222222222 - type: string - - name: reveal - required: false - in: query - description: |- - Boolean string. - - Truthy values: `true`, `1`, `yes`, `on`, `y`, `enabled` - - Falsy values: `false`, `0`, `no`, `off`, `n`, `disabled` - schema: - example: 'true' - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ApiKeyResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '404': - description: API key not found for project - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Get API key - tags: - - Secrets - x-badges: - - name: 'OAuth scope: secrets:read' - position: after - x-endpoint-owners: - - auth - - control-plane - x-fga-permissions: - - - api_gateway_keys_read - - - api_gateway_keys_read - - api_gateway_keys_secret_read - x-oauth-scope: secrets:read - delete: - operationId: v1-delete-project-api-key - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: id - required: true - in: path - schema: - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - example: 22222222-2222-4222-8222-222222222222 - type: string - - name: reveal - required: false - in: query - description: Boolean string, true or false - schema: - example: true - type: string - - name: was_compromised - required: false - in: query - description: Boolean string, true or false - schema: - example: false - type: string - - name: reason - required: false - in: query - schema: - example: rotating_key - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ApiKeyResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '404': - description: API key not found for project - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Deletes an API key for the project - tags: - - Secrets - x-badges: - - name: 'OAuth scope: secrets:write' - position: after - x-endpoint-owners: - - auth - - control-plane - x-fga-permissions: - - - api_gateway_keys_write - x-oauth-scope: secrets:write - /v1/projects/{ref}/branches: - get: - description: Returns all database branches of the specified project. - operationId: v1-list-all-branches - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/BranchResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve database branches - security: - - bearer: [] - summary: List all database branches - tags: - - Environments - x-badges: - - name: 'OAuth scope: environment:read' - position: after - x-endpoint-owners: - - dev-workflows - x-fga-permissions: - - - branching_development_read - - - branching_production_read - x-oauth-scope: environment:read - post: - description: Creates a database branch from the specified project. - operationId: v1-create-a-branch - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateBranchBody' - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/BranchResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to create database branch - security: - - bearer: [] - summary: Create a database branch - tags: - - Environments - x-badges: - - name: 'OAuth scope: environment:write' - position: after - x-endpoint-owners: - - dev-workflows - x-fga-permissions: - - - branching_development_create - - - branching_production_create - x-oauth-scope: environment:write - delete: - description: Disables preview branching for the specified project - operationId: v1-disable-preview-branching - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to disable preview branching - security: - - bearer: [] - summary: Disables preview branching - tags: - - Environments - x-badges: - - name: 'OAuth scope: environment:write' - position: after - x-endpoint-owners: - - dev-workflows - x-fga-permissions: - - - branching_production_delete - x-oauth-scope: environment:write - /v1/projects/{ref}/branches/{name}: - get: - description: Fetches the specified database branch by its name. - operationId: v1-get-a-branch - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: name - required: true - in: path - schema: - example: preview-login-page - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/BranchResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to fetch database branch - security: - - bearer: [] - summary: Get a database branch - tags: - - Environments - x-badges: - - name: 'OAuth scope: environment:read' - position: after - x-endpoint-owners: - - dev-workflows - x-fga-permissions: - - - branching_development_read - - - branching_production_read - x-oauth-scope: environment:read - /v1/projects/{ref}/custom-hostname: - get: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-get-hostname-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateCustomHostnameResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve project's custom hostname config - security: - - bearer: [] - summary: Gets project's custom hostname config - tags: - - Domains - x-badges: - - name: 'OAuth scope: domains:read' - position: after - - name: Beta - position: before - x-endpoint-owners: - - infra - - control-plane - x-fga-permissions: - - - custom_domain_read - x-oauth-scope: domains:read - x-scalar-stability: experimental - delete: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-Delete hostname config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: remove_addon - required: false - in: query - description: >- - If true, also removes the custom domain add-on from the project - subscription. - schema: - type: string - responses: - '200': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to delete project custom hostname configuration - security: - - bearer: [] - summary: Deletes a project's custom hostname configuration - tags: - - Domains - x-badges: - - name: 'OAuth scope: domains:write' - position: after - - name: Beta - position: before - x-endpoint-owners: - - infra - - control-plane - x-fga-permissions: - - - custom_domain_write - x-oauth-scope: domains:write - x-scalar-stability: experimental - /v1/projects/{ref}/custom-hostname/initialize: - post: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-update-hostname-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateCustomHostnameBody' - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateCustomHostnameResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to update project custom hostname configuration - security: - - bearer: [] - summary: Updates project's custom hostname configuration - tags: - - Domains - x-badges: - - name: 'OAuth scope: domains:write' - position: after - - name: Beta - position: before - x-endpoint-owners: - - infra - - control-plane - x-fga-permissions: - - - custom_domain_write - x-oauth-scope: domains:write - x-scalar-stability: experimental - /v1/projects/{ref}/custom-hostname/reverify: - post: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-verify-dns-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateCustomHostnameResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to verify project custom hostname configuration - security: - - bearer: [] - summary: >- - Attempts to verify the DNS configuration for project's custom hostname - configuration - tags: - - Domains - x-badges: - - name: 'OAuth scope: domains:write' - position: after - - name: Beta - position: before - x-endpoint-owners: - - infra - - control-plane - x-fga-permissions: - - - custom_domain_write - x-oauth-scope: domains:write - x-scalar-stability: experimental - /v1/projects/{ref}/custom-hostname/activate: - post: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-activate-custom-hostname - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateCustomHostnameResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to activate project custom hostname configuration - security: - - bearer: [] - summary: Activates a custom hostname for a project. - tags: - - Domains - x-badges: - - name: 'OAuth scope: domains:write' - position: after - - name: Beta - position: before - x-endpoint-owners: - - infra - - control-plane - x-fga-permissions: - - - custom_domain_write - x-oauth-scope: domains:write - x-scalar-stability: experimental - /v1/projects/{ref}/jit-access: - get: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-get-jit-access-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $schema: https://json-schema.org/draft/2020-12/schema - oneOf: - - type: object - properties: - state: - type: string - enum: - - enabled - - disabled - appliedSuccessfully: - type: boolean - required: - - state - - type: object - properties: - state: - type: string - const: unavailable - unavailableReason: - type: string - enum: - - platform_unsupported - - postgres_upgrade_required - - ssl_enforcement_required - - temporarily_unavailable - required: - - state - - unavailableReason - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve project's temporary access configuration. - security: - - bearer: [] - summary: Get project's temporary access configuration. - tags: - - Database - x-badges: - - name: 'OAuth scope: database:read' - position: after - - name: Beta - position: before - x-endpoint-owners: - - security - - control-plane - x-fga-permissions: - - - project_admin_read - x-oauth-scope: database:read - x-scalar-stability: experimental - put: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-update-jit-access-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/JitAccessRequestRequest' - responses: - '200': - description: '' - content: - application/json: - schema: - $schema: https://json-schema.org/draft/2020-12/schema - oneOf: - - type: object - properties: - state: - type: string - enum: - - enabled - - disabled - appliedSuccessfully: - type: boolean - required: - - state - - type: object - properties: - state: - type: string - const: unavailable - unavailableReason: - type: string - enum: - - platform_unsupported - - postgres_upgrade_required - - ssl_enforcement_required - - temporarily_unavailable - required: - - state - - unavailableReason - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to update project's temporary access configuration. - security: - - bearer: [] - summary: Update project's temporary access configuration. - tags: - - Database - x-badges: - - name: 'OAuth scope: database:write' - position: after - - name: Beta - position: before - x-endpoint-owners: - - security - - control-plane - x-fga-permissions: - - - project_admin_write - x-oauth-scope: database:write - x-scalar-stability: experimental - /v1/projects/{ref}/network-bans/retrieve: - post: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-list-all-network-bans - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/NetworkBanResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve project's network bans - security: - - bearer: [] - summary: Gets project's network bans - tags: - - Projects - x-badges: - - name: 'OAuth scope: projects:read' - position: after - - name: Beta - position: before - x-endpoint-owners: - - control-plane - - infra - x-fga-permissions: - - - database_network_bans_read - x-oauth-scope: projects:read - x-scalar-stability: experimental - /v1/projects/{ref}/network-bans/retrieve/enriched: - post: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-list-all-network-bans-enriched - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/NetworkBanResponseEnriched_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve project's enriched network bans - security: - - bearer: [] - summary: >- - Gets project's network bans with additional information about which - databases they affect - tags: - - Projects - x-badges: - - name: 'OAuth scope: projects:read' - position: after - - name: Beta - position: before - x-endpoint-owners: - - control-plane - - infra - x-fga-permissions: - - - database_network_bans_read - x-oauth-scope: projects:read - x-scalar-stability: experimental - /v1/projects/{ref}/network-bans: - delete: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-delete-network-bans - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/RemoveNetworkBanRequest' - responses: - '200': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to remove network bans. - security: - - bearer: [] - summary: Remove network bans. - tags: - - Projects - x-badges: - - name: 'OAuth scope: projects:write' - position: after - - name: Beta - position: before - x-endpoint-owners: - - control-plane - - infra - x-fga-permissions: - - - database_network_bans_write - x-oauth-scope: projects:write - x-scalar-stability: experimental - /v1/projects/{ref}/network-restrictions: - get: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-get-network-restrictions - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/NetworkRestrictionsResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve project's network restrictions - security: - - bearer: [] - summary: Gets project's network restrictions - tags: - - Projects - x-badges: - - name: 'OAuth scope: projects:read' - position: after - - name: Beta - position: before - x-endpoint-owners: - - infra - - control-plane - x-fga-permissions: - - - database_network_restrictions_read - x-oauth-scope: projects:read - x-scalar-stability: experimental - patch: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Alpha** stage. - operationId: v1-patch-network-restrictions - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/NetworkRestrictionsPatchRequest' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/NetworkRestrictionsV2Response_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to update project network restrictions - security: - - bearer: [] - summary: Updates project's network restrictions by adding or removing CIDRs - tags: - - Projects - x-badges: - - name: 'OAuth scope: projects:write' - position: after - - name: Alpha - position: before - x-endpoint-owners: - - infra - - control-plane - x-fga-permissions: - - - database_network_restrictions_write - x-oauth-scope: projects:write - x-scalar-stability: experimental - /v1/projects/{ref}/network-restrictions/apply: - post: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-update-network-restrictions - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/NetworkRestrictionsRequest' - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/NetworkRestrictionsResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to update project network restrictions - security: - - bearer: [] - summary: Updates project's network restrictions - tags: - - Projects - x-badges: - - name: 'OAuth scope: projects:write' - position: after - - name: Beta - position: before - x-endpoint-owners: - - infra - - control-plane - x-fga-permissions: - - - database_network_restrictions_write - x-oauth-scope: projects:write - x-scalar-stability: experimental - /v1/projects/{ref}/pgsodium: - get: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-get-pgsodium-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/PgsodiumConfigResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve project's pgsodium config - security: - - bearer: [] - summary: Gets project's pgsodium config - tags: - - Secrets - x-badges: - - name: 'OAuth scope: secrets:read' - position: after - - name: Beta - position: before - x-endpoint-owners: - - infra - x-fga-permissions: - - - project_admin_write - x-oauth-scope: secrets:read - x-scalar-stability: experimental - put: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-update-pgsodium-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdatePgsodiumConfigBody' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/PgsodiumConfigResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to update project's pgsodium config - security: - - bearer: [] - summary: >- - Updates project's pgsodium config. Updating the root_key can cause all - data encrypted with the older key to become inaccessible. - tags: - - Secrets - x-badges: - - name: 'OAuth scope: secrets:write' - position: after - - name: Beta - position: before - x-endpoint-owners: - - infra - x-fga-permissions: - - - project_admin_write - x-oauth-scope: secrets:write - x-scalar-stability: experimental - /v1/projects/{ref}/postgrest: - get: - operationId: v1-get-postgrest-service-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/PostgrestConfigWithJWTSecretResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve project's postgrest config - security: - - bearer: [] - summary: Gets project's postgrest config - tags: - - Rest - x-badges: - - name: 'OAuth scope: rest:read' - position: after - x-endpoint-owners: - - control-plane - - infra - x-fga-permissions: - - - data_api_config_read - - - data_api_config_read - - data_api_config_secret_read - x-oauth-scope: rest:read - patch: - operationId: v1-update-postgrest-service-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/V1UpdatePostgrestConfigBody' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/V1PostgrestConfigResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to update project's postgrest config - security: - - bearer: [] - summary: Updates project's postgrest config - tags: - - Rest - x-badges: - - name: 'OAuth scope: rest:write' - position: after - x-endpoint-owners: - - control-plane - - infra - x-fga-permissions: - - - data_api_config_write - x-oauth-scope: rest:write - /v1/projects/{ref}: - get: - operationId: v1-get-project - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/V1ProjectWithDatabaseResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve project - security: - - bearer: [] - summary: Gets a specific project that belongs to the authenticated user - tags: - - Projects - x-badges: - - name: 'OAuth scope: projects:read' - position: after - x-endpoint-owners: - - control-plane - x-fga-permissions: - - - project_admin_read - x-oauth-scope: projects:read - delete: - operationId: v1-delete-a-project - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/V1ProjectRefResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Deletes the given project - tags: - - Projects - x-badges: - - name: 'OAuth scope: projects:write' - position: after - x-endpoint-owners: - - control-plane - - infra - - dev-workflows - x-fga-permissions: - - - project_admin_write - x-oauth-scope: projects:write - patch: - operationId: v1-update-a-project - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/V1UpdateProjectBody' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/V1ProjectRefResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to update project - security: - - bearer: [] - summary: Updates the given project - tags: - - Projects - x-badges: - - name: 'OAuth scope: projects:write' - position: after - x-endpoint-owners: - - control-plane - x-fga-permissions: - - - project_admin_write - x-oauth-scope: projects:write - /v1/projects/{ref}/secrets: - get: - description: Returns all secrets you've previously added to the specified project. - operationId: v1-list-all-secrets - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/SecretResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve project's secrets - security: - - bearer: [] - summary: List all secrets - tags: - - Secrets - x-badges: - - name: 'OAuth scope: secrets:read' - position: after - x-endpoint-owners: - - functions - x-fga-permissions: - - - edge_functions_secrets_read - x-oauth-scope: secrets:read - post: - description: Creates multiple secrets and adds them to the specified project. - operationId: v1-bulk-create-secrets - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateSecretBody' - responses: - '201': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to create project's secrets - security: - - bearer: [] - summary: Bulk create secrets - tags: - - Secrets - x-badges: - - name: 'OAuth scope: secrets:write' - position: after - x-endpoint-owners: - - functions - x-fga-permissions: - - - edge_functions_secrets_write - x-oauth-scope: secrets:write - delete: - description: Deletes all secrets with the given names from the specified project - operationId: v1-bulk-delete-secrets - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/DeleteSecretsBody' - responses: - '200': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to delete secrets with given names - security: - - bearer: [] - summary: Bulk delete secrets - tags: - - Secrets - x-badges: - - name: 'OAuth scope: secrets:write' - position: after - x-endpoint-owners: - - functions - x-fga-permissions: - - - edge_functions_secrets_write - x-oauth-scope: secrets:write - /v1/projects/{ref}/ssl-enforcement: - get: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-get-ssl-enforcement-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/SslEnforcementResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve project's SSL enforcement config - security: - - bearer: [] - summary: Get project's SSL enforcement configuration. - tags: - - Database - x-badges: - - name: 'OAuth scope: database:read' - position: after - - name: Beta - position: before - x-endpoint-owners: - - infra - - control-plane - x-fga-permissions: - - - database_ssl_config_read - x-oauth-scope: database:read - x-scalar-stability: experimental - put: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-update-ssl-enforcement-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SslEnforcementRequest' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/SslEnforcementResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to update project's SSL enforcement configuration. - security: - - bearer: [] - summary: Update project's SSL enforcement configuration. - tags: - - Database - x-badges: - - name: 'OAuth scope: database:write' - position: after - - name: Beta - position: before - x-endpoint-owners: - - infra - - control-plane - x-fga-permissions: - - - database_ssl_config_write - x-oauth-scope: database:write - x-scalar-stability: experimental - /v1/projects/{ref}/types/typescript: - get: - description: Returns the TypeScript types of your schema for use with supabase-js. - operationId: v1-generate-typescript-types - parameters: - - name: included_schemas - required: false - in: query - schema: - default: public - example: public,auth - type: string - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/TypescriptResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to generate TypeScript types - security: - - bearer: [] - summary: Generate TypeScript types - tags: - - Database - x-badges: - - name: 'OAuth scope: database:read' - position: after - x-endpoint-owners: - - dev-workflows - x-fga-permissions: - - - database_read - x-oauth-scope: database:read - /v1/projects/{ref}/vanity-subdomain: - get: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-get-vanity-subdomain-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/VanitySubdomainConfigResponse_Output' - '400': - description: >- - This feature requires the Pro, Team, or Enterprise organization - plan. - content: - application/json: - schema: - $ref: '#/components/schemas/PlanGateErrorBody' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to get project vanity subdomain configuration - security: - - bearer: [] - summary: Gets current vanity subdomain config - tags: - - Domains - x-allowed-plans: - - Pro - - Team - - Enterprise - x-badges: - - name: 'OAuth scope: domains:read' - position: after - - name: Beta - position: before - - name: Only available on Pro, Team, Enterprise - position: before - x-endpoint-owners: - - infra - - control-plane - x-fga-permissions: - - - vanity_subdomain_read - x-oauth-scope: domains:read - x-scalar-stability: experimental - delete: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-deactivate-vanity-subdomain-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to delete project vanity subdomain configuration - security: - - bearer: [] - summary: Deletes a project's vanity subdomain configuration - tags: - - Domains - x-badges: - - name: 'OAuth scope: domains:write' - position: after - - name: Beta - position: before - x-endpoint-owners: - - infra - - control-plane - x-fga-permissions: - - - vanity_subdomain_write - x-oauth-scope: domains:write - x-scalar-stability: experimental - /v1/projects/{ref}/vanity-subdomain/check-availability: - post: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-check-vanity-subdomain-availability - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/VanitySubdomainBody' - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/SubdomainAvailabilityResponse_Output' - '400': - description: >- - This feature requires the Pro, Team, or Enterprise organization - plan. - content: - application/json: - schema: - $ref: '#/components/schemas/PlanGateErrorBody' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to check project vanity subdomain configuration - security: - - bearer: [] - summary: Checks vanity subdomain availability - tags: - - Domains - x-allowed-plans: - - Pro - - Team - - Enterprise - x-badges: - - name: 'OAuth scope: domains:write' - position: after - - name: Beta - position: before - - name: Only available on Pro, Team, Enterprise - position: before - x-endpoint-owners: - - infra - - control-plane - x-fga-permissions: - - - vanity_subdomain_write - x-oauth-scope: domains:write - x-scalar-stability: experimental - /v1/projects/{ref}/vanity-subdomain/activate: - post: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-activate-vanity-subdomain-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/VanitySubdomainBody' - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ActivateVanitySubdomainResponse_Output' - '400': - description: >- - This feature requires the Pro, Team, or Enterprise organization - plan. - content: - application/json: - schema: - $ref: '#/components/schemas/PlanGateErrorBody' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to activate project vanity subdomain configuration - security: - - bearer: [] - summary: Activates a vanity subdomain for a project. - tags: - - Domains - x-allowed-plans: - - Pro - - Team - - Enterprise - x-badges: - - name: 'OAuth scope: domains:write' - position: after - - name: Beta - position: before - - name: Only available on Pro, Team, Enterprise - position: before - x-endpoint-owners: - - infra - - control-plane - x-fga-permissions: - - - vanity_subdomain_write - x-oauth-scope: domains:write - x-scalar-stability: experimental - /v1/projects/{ref}/upgrade: - post: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-upgrade-postgres-version - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpgradeDatabaseBody' - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ProjectUpgradeInitiateResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to initiate project upgrade - security: - - bearer: [] - summary: Upgrades the project's Postgres version - tags: - - Projects - x-badges: - - name: 'OAuth scope: projects:write' - position: after - - name: Beta - position: before - x-endpoint-owners: - - control-plane - - infra - x-fga-permissions: - - - project_admin_write - - database_write - x-oauth-scope: projects:write - x-scalar-stability: experimental - /v1/projects/{ref}/upgrade/eligibility: - get: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-get-postgres-upgrade-eligibility - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ProjectUpgradeEligibilityResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to determine project upgrade eligibility - security: - - bearer: [] - summary: Returns the project's eligibility for upgrades - tags: - - Projects - x-badges: - - name: 'OAuth scope: projects:read' - position: after - - name: Beta - position: before - x-endpoint-owners: - - control-plane - - infra - x-fga-permissions: - - - project_admin_read - - database_read - x-oauth-scope: projects:read - x-scalar-stability: experimental - /v1/projects/{ref}/upgrade/status: - get: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-get-postgres-upgrade-status - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: tracking_id - required: false - in: query - schema: - example: 9f4d3a20-6b2e-4a7e-8c91-1d5f3e7a2b4c - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/DatabaseUpgradeStatusResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve project upgrade status - security: - - bearer: [] - summary: Gets the latest status of the project's upgrade - tags: - - Projects - x-badges: - - name: 'OAuth scope: projects:read' - position: after - - name: Beta - position: before - x-endpoint-owners: - - control-plane - - infra - x-fga-permissions: - - - project_admin_read - - database_read - x-oauth-scope: projects:read - x-scalar-stability: experimental - /v1/projects/{ref}/readonly: - get: - operationId: v1-get-readonly-mode-status - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ReadOnlyStatusResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to get project readonly mode status - security: - - bearer: [] - summary: Returns project's readonly mode status - tags: - - Database - x-badges: - - name: 'OAuth scope: database:read' - position: after - x-endpoint-owners: - - control-plane - - infra - - support-tooling - x-fga-permissions: - - - database_readonly_config_read - x-oauth-scope: database:read - /v1/projects/{ref}/readonly/temporary-disable: - post: - operationId: v1-disable-readonly-mode-temporarily - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '201': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to disable project's readonly mode - security: - - bearer: [] - summary: Disables project's readonly mode for the next 15 minutes - tags: - - Database - x-badges: - - name: 'OAuth scope: database:write' - position: after - x-endpoint-owners: - - control-plane - - infra - - support-tooling - x-fga-permissions: - - - database_readonly_config_write - x-oauth-scope: database:write - /v1/projects/{ref}/read-replicas/setup: - post: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-setup-a-read-replica - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SetUpReadReplicaBody' - responses: - '204': - description: '' - '401': - description: Unauthorized - '402': - description: >- - This feature requires the Pro, Team, or Enterprise organization - plan. - content: - application/json: - schema: - $ref: '#/components/schemas/PlanGateErrorBody' - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to set up read replica - security: - - bearer: [] - summary: Set up a read replica - tags: - - Database - x-allowed-plans: - - Pro - - Team - - Enterprise - x-badges: - - name: Beta - position: before - - name: Only available on Pro, Team, Enterprise - position: before - x-endpoint-owners: - - control-plane - - infra - x-fga-permissions: - - - infra_read_replicas_write - x-scalar-stability: experimental - /v1/projects/{ref}/read-replicas/remove: - post: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-remove-a-read-replica - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/RemoveReadReplicaBody' - responses: - '204': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to remove read replica - security: - - bearer: [] - summary: Remove a read replica - tags: - - Database - x-badges: - - name: Beta - position: before - x-endpoint-owners: - - control-plane - - infra - x-fga-permissions: - - - infra_read_replicas_write - x-scalar-stability: experimental - /v1/projects/{ref}/health: - get: - operationId: v1-get-services-health - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: services - required: true - in: query - description: Comma-separated list of enums or array of enums. - schema: - example: - - auth,db - - auth - anyOf: - - type: string - description: |- - Comma-separated list of enums: - - - `auth` - - `db` - - `db_postgres_user` - - `pooler` - - `realtime` - - `rest` - - `storage` - - `pg_bouncer` - example: - - auth,db - - auth - - type: array - items: - type: string - enum: - - auth - - db - - db_postgres_user - - pooler - - realtime - - rest - - storage - - pg_bouncer - description: Array of enums. - example: - - '{field}=auth&{field}=db' - - '{field}=auth' - - name: timeout_ms - required: false - in: query - schema: - minimum: 0 - maximum: 10000 - example: 2000 - type: integer - responses: - '200': - description: '' - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/V1ServiceHealthResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve project's service health status - security: - - bearer: [] - summary: Gets project's service health status - tags: - - Projects - x-badges: - - name: 'OAuth scope: projects:read' - position: after - x-endpoint-owners: - - control-plane - - infra - x-fga-permissions: - - - project_admin_read - x-oauth-scope: projects:read - /v1/projects/{ref}/config/auth/signing-keys/legacy: - post: - operationId: v1-create-legacy-signing-key - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/SigningKeyResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: >- - Set up the project's existing JWT secret as an in_use JWT signing key. - This endpoint will be removed in the future always check for HTTP 404 - Not Found. - tags: - - Auth - x-badges: - - name: 'OAuth scope: secrets:write' - position: after - x-endpoint-owners: - - auth - x-fga-permissions: - - - auth_signing_keys_write - x-oauth-scope: secrets:write - get: - operationId: v1-get-legacy-signing-key - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/SigningKeyResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: >- - Get the signing key information for the JWT secret imported as signing - key for this project. This endpoint will be removed in the future, check - for HTTP 404 Not Found. - tags: - - Auth - x-badges: - - name: 'OAuth scope: secrets:read' - position: after - x-endpoint-owners: - - auth - x-fga-permissions: - - - auth_signing_keys_read - x-oauth-scope: secrets:read - /v1/projects/{ref}/config/auth/signing-keys: - post: - operationId: v1-create-project-signing-key - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateSigningKeyBody' - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/SigningKeyResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Create a new signing key for the project in standby status - tags: - - Auth - x-badges: - - name: 'OAuth scope: secrets:write' - position: after - x-endpoint-owners: - - auth - x-fga-permissions: - - - auth_signing_keys_write - x-oauth-scope: secrets:write - get: - operationId: v1-get-project-signing-keys - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/SigningKeysResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: List all signing keys for the project - tags: - - Auth - x-badges: - - name: 'OAuth scope: secrets:read' - position: after - x-endpoint-owners: - - auth - x-fga-permissions: - - - auth_signing_keys_read - x-oauth-scope: secrets:read - /v1/projects/{ref}/config/auth/signing-keys/{id}: - get: - operationId: v1-get-project-signing-key - parameters: - - name: id - required: true - in: path - schema: - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - example: 33333333-3333-4333-8333-333333333333 - type: string - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/SigningKeyResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Get information about a signing key - tags: - - Auth - x-endpoint-owners: - - auth - x-fga-permissions: - - - auth_signing_keys_read - delete: - operationId: v1-remove-project-signing-key - parameters: - - name: id - required: true - in: path - schema: - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - example: 33333333-3333-4333-8333-333333333333 - type: string - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/SigningKeyResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: >- - Remove a signing key from a project. Only possible if the key has been - in revoked status for a while. - tags: - - Auth - x-badges: - - name: 'OAuth scope: secrets:write' - position: after - x-endpoint-owners: - - auth - x-fga-permissions: - - - auth_signing_keys_write - x-oauth-scope: secrets:write - patch: - operationId: v1-update-project-signing-key - parameters: - - name: id - required: true - in: path - schema: - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - example: 33333333-3333-4333-8333-333333333333 - type: string - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateSigningKeyBody' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/SigningKeyResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Update a signing key, mainly its status - tags: - - Auth - x-badges: - - name: 'OAuth scope: secrets:write' - position: after - x-endpoint-owners: - - auth - x-fga-permissions: - - - auth_signing_keys_write - x-oauth-scope: secrets:write - /v1/projects/{ref}/config/auth: - get: - operationId: v1-get-auth-service-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/AuthConfigResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve project's auth config - security: - - bearer: [] - summary: Gets project's auth config - tags: - - Auth - x-badges: - - name: 'OAuth scope: auth:read' - position: after - x-endpoint-owners: - - auth - x-fga-permissions: - - - auth_config_read - x-oauth-scope: auth:read - patch: - operationId: v1-update-auth-service-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateAuthConfigBody' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/AuthConfigResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to update project's auth config - security: - - bearer: [] - summary: Updates a project's auth config - tags: - - Auth - x-badges: - - name: 'OAuth scope: auth:write' - position: after - x-endpoint-owners: - - auth - x-fga-permissions: - - - auth_config_write - - project_admin_write - x-oauth-scope: auth:write - /v1/projects/{ref}/config/auth/third-party-auth: - post: - operationId: v1-create-project-tpa-integration - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateThirdPartyAuthBody' - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ThirdPartyAuth_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Creates a new third-party auth integration - tags: - - Auth - x-badges: - - name: 'OAuth scope: auth:write' - position: after - x-endpoint-owners: - - auth - x-fga-permissions: - - - auth_config_write - x-oauth-scope: auth:write - get: - operationId: v1-list-project-tpa-integrations - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/ThirdPartyAuth_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Lists all third-party auth integrations - tags: - - Auth - x-badges: - - name: 'OAuth scope: auth:read' - position: after - x-endpoint-owners: - - auth - x-fga-permissions: - - - auth_config_read - x-oauth-scope: auth:read - /v1/projects/{ref}/config/auth/third-party-auth/{tpa_id}: - delete: - operationId: v1-delete-project-tpa-integration - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: tpa_id - required: true - in: path - schema: - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - example: 88888888-8888-4888-8888-888888888888 - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ThirdPartyAuth_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Removes a third-party auth integration - tags: - - Auth - x-badges: - - name: 'OAuth scope: auth:write' - position: after - x-endpoint-owners: - - auth - x-fga-permissions: - - - auth_config_write - x-oauth-scope: auth:write - get: - operationId: v1-get-project-tpa-integration - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: tpa_id - required: true - in: path - schema: - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - example: 88888888-8888-4888-8888-888888888888 - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ThirdPartyAuth_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Get a third-party integration - tags: - - Auth - x-badges: - - name: 'OAuth scope: auth:read' - position: after - x-endpoint-owners: - - auth - x-fga-permissions: - - - auth_config_read - x-oauth-scope: auth:read - /v1/projects/{ref}/pause: - post: - operationId: v1-pause-a-project - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Pauses the given project - tags: - - Projects - x-badges: - - name: 'OAuth scope: projects:write' - position: after - x-endpoint-owners: - - infra - - control-plane - x-fga-permissions: - - - project_admin_write - x-oauth-scope: projects:write - /v1/projects/{ref}/restart: - post: - operationId: v1-restart-a-project - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Restarts the given project - tags: - - Projects - x-badges: - - name: 'OAuth scope: projects:write' - position: after - x-endpoint-owners: - - infra - - control-plane - x-fga-permissions: - - - project_admin_write - x-oauth-scope: projects:write - /v1/projects/{ref}/restore: - get: - operationId: v1-list-available-restore-versions - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/GetProjectAvailableRestoreVersionsResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Lists available restore versions for the given project - tags: - - Projects - x-badges: - - name: 'OAuth scope: projects:read' - position: after - x-endpoint-owners: - - control-plane - - infra - x-fga-permissions: - - - project_admin_read - x-oauth-scope: projects:read - post: - operationId: v1-restore-a-project - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Restores the given project - tags: - - Projects - x-badges: - - name: 'OAuth scope: projects:write' - position: after - x-endpoint-owners: - - control-plane - - infra - x-fga-permissions: - - - project_admin_write - x-oauth-scope: projects:write - /v1/projects/{ref}/restore/cancel: - post: - operationId: v1-cancel-a-project-restoration - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Cancels the given project restoration - tags: - - Projects - x-badges: - - name: 'OAuth scope: projects:write' - position: after - x-endpoint-owners: - - control-plane - - infra - x-fga-permissions: - - - project_admin_write - x-oauth-scope: projects:write - /v1/projects/{ref}/billing/addons: - get: - description: >- - Returns the billing addons that are currently applied, including the - active compute instance size, and lists every addon option that can be - provisioned with pricing metadata. - operationId: v1-list-project-addons - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ListProjectAddonsResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to list project addons - security: - - bearer: [] - summary: List billing addons and compute instance selections - tags: - - Billing - x-endpoint-owners: - - billing - x-fga-permissions: - - - infra_add_ons_read - patch: - description: >- - Selects an addon variant, for example scaling the project’s compute - instance up or down, and applies it to the project. - operationId: v1-apply-project-addon - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/ApplyProjectAddonBody' - responses: - '200': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to apply project addon - security: - - bearer: [] - summary: Apply or update billing addons, including compute instance size - tags: - - Billing - x-endpoint-owners: - - billing - x-fga-permissions: - - - infra_add_ons_write - /v1/projects/{ref}/billing/addons/{addon_variant}: - delete: - description: >- - Disables the selected addon variant, including rolling the compute - instance back to its previous size. - operationId: v1-remove-project-addon - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: addon_variant - required: true - in: path - schema: - example: pitr_7 - anyOf: - - type: string - enum: - - ci_micro - - ci_small - - ci_medium - - ci_large - - ci_xlarge - - ci_2xlarge - - ci_4xlarge - - ci_8xlarge - - ci_12xlarge - - ci_16xlarge - - ci_24xlarge - - ci_24xlarge_optimized_cpu - - ci_24xlarge_optimized_memory - - ci_24xlarge_high_memory - - ci_48xlarge - - ci_48xlarge_optimized_cpu - - ci_48xlarge_optimized_memory - - ci_48xlarge_high_memory - - type: string - enum: - - cd_default - - type: string - enum: - - pitr_7 - - pitr_14 - - pitr_28 - - type: string - enum: - - ipv4_default - responses: - '200': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to remove project addon - security: - - bearer: [] - summary: Remove billing addons or revert compute instance sizing - tags: - - Billing - x-endpoint-owners: - - billing - x-fga-permissions: - - - infra_add_ons_write - /v1/projects/{ref}/claim-token: - get: - operationId: v1-get-project-claim-token - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ProjectClaimTokenResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Gets project claim token - tags: - - Projects - x-endpoint-owners: - - control-plane - x-fga-permissions: - - - project_admin_read - x-internal: true - post: - operationId: v1-create-project-claim-token - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/CreateProjectClaimTokenResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Creates project claim token - tags: - - Projects - x-endpoint-owners: - - control-plane - x-fga-permissions: - - - organization_admin_write - - project_admin_write - x-internal: true - delete: - operationId: v1-delete-project-claim-token - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '204': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Revokes project claim token - tags: - - Projects - x-endpoint-owners: - - control-plane - x-fga-permissions: - - - organization_admin_write - - project_admin_write - x-internal: true - /v1/projects/{ref}/advisors/performance: - get: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - operationId: v1-get-performance-advisors - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/V1ProjectAdvisorsResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Gets project performance advisors. - tags: - - Advisors - x-badges: - - name: 'OAuth scope: database:read' - position: after - x-endpoint-owners: - - control-plane - x-fga-permissions: - - - advisors_read - x-oauth-scope: database:read - x-scalar-stability: experimental - /v1/projects/{ref}/advisors/security: - get: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - operationId: v1-get-security-advisors - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: lint_type - required: false - in: query - schema: - example: sql - type: string - enum: - - sql - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/V1ProjectAdvisorsResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Gets project security advisors. - tags: - - Advisors - x-badges: - - name: 'OAuth scope: database:read' - position: after - x-endpoint-owners: - - control-plane - x-fga-permissions: - - - advisors_read - x-oauth-scope: database:read - x-scalar-stability: experimental - /v1/projects/{ref}/analytics/endpoints/logs.all: - get: - deprecated: true - description: >- - This endpoint has been removed and always responds with `410 Gone`. Use - `GET /v1/projects/{ref}/analytics/endpoints/logs` instead. See the - [migration - guide](https://supabase.com/changelog/48235-migration-of-supabase-management-api-logs-all-analytics-endpoint-to-logs-endpoint). - operationId: v1-get-project-logs-all - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '401': - description: Unauthorized - '403': - description: Forbidden action - '410': - description: This endpoint has been removed - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Gets project's logs - tags: - - Analytics - x-badges: - - name: 'OAuth scope: analytics:read' - position: after - x-endpoint-owners: - - observability - x-fga-permissions: - - - analytics_logs_read - x-oauth-scope: analytics:read - /v1/projects/{ref}/analytics/endpoints/logs: - get: - deprecated: false - description: > - Executes an SQL or LQL query on the project's unified logs stream. - - - Either the `iso_timestamp_start` and `iso_timestamp_end` parameters must - be provided. - - If both are not provided, only the last 1 minute of logs will be - queried. - - The timestamp range must be no more than 24 hours and is rounded to the - nearest minute. If the range is more than 24 hours, a validation error - will be thrown. - - - Filter by the `source` column to specify specific log sources, such as - edge_logs, postgres_logs, etc. - - - Note: SQL must be written in **ClickHouse SQL dialect**. - operationId: v1-get-project-logs - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: sql - required: false - in: query - description: >- - Custom SQL query to execute on the logs. See [querying - logs](https://supabase.com/docs/guides/monitoring-and-debugging/logs#querying-with-the-logs-explorer) - for more details. - schema: - example: select event_message from edge_logs limit 10 - type: string - - name: iso_timestamp_start - required: false - in: query - schema: - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ - example: '2025-03-01T00:00:00Z' - type: string - - name: iso_timestamp_end - required: false - in: query - schema: - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ - example: '2025-03-01T23:59:59Z' - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/AnalyticsResponse_Output' - '401': - description: Unauthorized - '402': - description: Usage exceeded. Enable additional usage to continue querying - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Gets all project's logs in a single log stream - tags: - - Analytics - x-badges: - - name: 'OAuth scope: analytics:read' - position: after - x-endpoint-owners: - - observability - x-fga-permissions: - - - analytics_logs_read - x-oauth-scope: analytics:read - /v1/projects/{ref}/analytics/endpoints/usage.api-counts: - get: - operationId: v1-get-project-usage-api-count - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: interval - required: false - in: query - schema: - example: 1day - type: string - enum: - - 15min - - 30min - - 1hr - - 3hr - - 1day - - 3day - - 7day - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/V1GetUsageApiCountResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to get project's usage api counts - security: - - bearer: [] - summary: Gets project's usage api counts - tags: - - Analytics - x-endpoint-owners: - - observability - x-fga-permissions: - - - analytics_usage_read - /v1/projects/{ref}/analytics/endpoints/usage.api-requests-count: - get: - operationId: v1-get-project-usage-request-count - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/V1GetUsageApiRequestsCountResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to get project's usage api requests count - security: - - bearer: [] - summary: Gets project's usage api requests count - tags: - - Analytics - x-endpoint-owners: - - observability - x-fga-permissions: - - - analytics_usage_read - /v1/projects/{ref}/analytics/endpoints/functions.combined-stats: - get: - operationId: v1-get-project-function-combined-stats - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: interval - required: true - in: query - schema: - example: 1hr - type: string - enum: - - 15min - - 1hr - - 3hr - - 1day - - name: function_id - required: true - in: query - schema: - example: 3c078cce-ad70-4148-9f37-4da362789053 - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/AnalyticsResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to get project's function combined statistics - security: - - bearer: [] - summary: Gets a project's function combined statistics - tags: - - Analytics - x-endpoint-owners: - - observability - x-fga-permissions: - - - analytics_usage_read - /v1/projects/{ref}/analytics/endpoints/metrics: - get: - description: >- - Prometheus scrape endpoint. Returns metrics of a customer project in the - Prometheus open exposition format. - operationId: v1-scrape-project-metrics - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: Prometheus / OpenMetrics text exposition - content: - text/plain: - schema: - type: string - application/openmetrics-text: - schema: - type: string - '400': - description: >- - Project must be active and healthy, or metrics are not available for - this project - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to fetch project's metrics - security: - - bearer: [] - summary: Scrape a project's metrics - tags: - - Analytics - x-badges: - - name: 'OAuth scope: analytics:read' - position: after - x-endpoint-owners: - - observability - x-fga-permissions: - - - analytics_logs_read - x-oauth-scope: analytics:read - /v1/projects/{ref}/cli/login-role: - post: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-create-login-role - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateRoleBody' - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/CreateRoleResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to create login role - security: - - bearer: [] - summary: Create a login role for CLI with temporary password - tags: - - Database - x-badges: - - name: 'OAuth scope: database:write' - position: after - - name: Beta - position: before - x-endpoint-owners: - - dev-workflows - x-fga-permissions: - - - database_write - x-oauth-scope: database:write - x-scalar-stability: experimental - delete: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-delete-login-roles - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/DeleteRolesResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to delete login roles - security: - - bearer: [] - summary: Delete existing login roles used by CLI - tags: - - Database - x-badges: - - name: 'OAuth scope: database:write' - position: after - - name: Beta - position: before - x-endpoint-owners: - - dev-workflows - x-fga-permissions: - - - database_write - x-oauth-scope: database:write - x-scalar-stability: experimental - /v1/projects/{ref}/database/migrations: - get: - operationId: v1-list-migration-history - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/V1ListMigrationsResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to list database migrations - security: - - bearer: [] - summary: List applied migration versions - tags: - - Database - x-badges: - - name: 'OAuth scope: database:read' - position: after - x-endpoint-owners: - - infra - x-fga-permissions: - - - database_migrations_read - x-oauth-scope: database:read - post: - operationId: v1-apply-a-migration - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: Idempotency-Key - required: false - in: header - description: A unique key to ensure the same migration is tracked only once. - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/V1CreateMigrationBody' - responses: - '200': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to apply database migration - security: - - bearer: [] - summary: Apply a database migration - tags: - - Database - x-badges: - - name: 'OAuth scope: database:write' - position: after - x-endpoint-owners: - - infra - x-fga-permissions: - - - database_migrations_write - x-oauth-scope: database:write - put: - operationId: v1-upsert-a-migration - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: Idempotency-Key - required: false - in: header - description: A unique key to ensure the same migration is tracked only once. - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/V1UpsertMigrationBody' - responses: - '200': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to upsert database migration - security: - - bearer: [] - summary: Upsert a database migration without applying - tags: - - Database - x-badges: - - name: 'OAuth scope: database:write' - position: after - x-endpoint-owners: - - infra - x-fga-permissions: - - - database_migrations_write - x-oauth-scope: database:write - delete: - operationId: v1-rollback-migrations - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: gte - required: true - in: query - description: Rollback migrations greater or equal to this version - schema: - pattern: ^\d+$ - example: '20250312000000' - type: string - responses: - '200': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to rollback database migration - security: - - bearer: [] - summary: Rollback database migrations and remove them from history table - tags: - - Database - x-badges: - - name: 'OAuth scope: database:write' - position: after - x-endpoint-owners: - - infra - x-fga-permissions: - - - database_migrations_write - x-oauth-scope: database:write - /v1/projects/{ref}/database/migrations/{version}: - get: - operationId: v1-get-a-migration - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: version - required: true - in: path - schema: - pattern: ^\d+$ - example: '20250312000000' - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/V1GetMigrationResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to get database migration - security: - - bearer: [] - summary: Fetch an existing entry from migration history - tags: - - Database - x-badges: - - name: 'OAuth scope: database:read' - position: after - x-endpoint-owners: - - infra - x-fga-permissions: - - - database_migrations_read - x-oauth-scope: database:read - patch: - operationId: v1-patch-a-migration - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: version - required: true - in: path - schema: - pattern: ^\d+$ - example: '20250312000000' - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/V1PatchMigrationBody' - responses: - '200': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to patch database migration - security: - - bearer: [] - summary: Patch an existing entry in migration history - tags: - - Database - x-badges: - - name: 'OAuth scope: database:write' - position: after - x-endpoint-owners: - - infra - x-fga-permissions: - - - database_migrations_write - x-oauth-scope: database:write - /v1/projects/{ref}/database/query: - post: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-run-a-query - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/V1RunQueryBody' - responses: - '201': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to run sql query - security: - - bearer: [] - summary: Run sql query - tags: - - Database - x-badges: - - name: 'OAuth scope: database:write' - position: after - - name: Beta - position: before - x-endpoint-owners: - - control-plane - x-fga-permissions: - - - database_read - - - database_write - x-oauth-scope: database:write - x-scalar-stability: experimental - /v1/projects/{ref}/database/query/read-only: - post: - description: |- - All entity references must be schema qualified. - - This endpoint is currently in its **Beta** stage. - operationId: v1-read-only-query - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/V1ReadOnlyQueryBody' - responses: - '201': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to run read-only sql query - security: - - bearer: [] - summary: Run a sql query as supabase_read_only_user - tags: - - Database - x-badges: - - name: 'OAuth scope: database:read' - position: after - - name: Beta - position: before - x-endpoint-owners: - - control-plane - x-fga-permissions: - - - database_read - x-oauth-scope: database:read - x-scalar-stability: experimental - /v1/projects/{ref}/database/webhooks/enable: - post: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - - - This endpoint is currently in its **Beta** stage. - operationId: v1-enable-database-webhook - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '201': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to enable Database Webhooks on the project - security: - - bearer: [] - summary: Enables Database Webhooks on the project - tags: - - Database - x-badges: - - name: 'OAuth scope: database:write' - position: after - - name: Beta - position: before - x-endpoint-owners: - - control-plane - - infra - x-fga-permissions: - - - database_webhooks_config_write - x-oauth-scope: database:write - x-scalar-stability: experimental - /v1/projects/{ref}/database/context: - get: - description: >- - This is an **experimental** endpoint. It is subject to change or removal - in future versions. Use it with caution, as it may not remain supported - or stable. - operationId: v1-get-database-metadata - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/GetProjectDbMetadataResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Gets database metadata for the given project. - tags: - - Database - x-badges: - - name: 'OAuth scope: projects:read' - position: after - x-endpoint-owners: - - control-plane - x-fga-permissions: - - - database_read - x-oauth-scope: projects:read - x-scalar-stability: experimental - /v1/projects/{ref}/database/password: - patch: - operationId: v1-update-database-password - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/V1UpdatePasswordBody' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/V1UpdatePasswordResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to update database password - security: - - bearer: [] - summary: Updates the database password - tags: - - Database - x-badges: - - name: 'OAuth scope: database:write' - position: after - x-endpoint-owners: - - control-plane - - infra - x-fga-permissions: - - - database_config_write - x-oauth-scope: database:write - /v1/projects/{ref}/database/jit: - get: - description: Mappings of roles a user can assume in the project database - operationId: v1-get-jit-access - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/JitAccessResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to list database jit access - security: - - bearer: [] - summary: Get user-id to role mappings for JIT access - tags: - - Database - x-badges: - - name: 'OAuth scope: database:read' - position: after - x-endpoint-owners: - - security - x-fga-permissions: - - - database_jit_read - x-oauth-scope: database:read - post: - description: Authorizes the request to assume a role in the project database - operationId: v1-authorize-jit-access - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/AuthorizeJitAccessBody' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/JitAuthorizeAccessResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to authorize database jit access - security: - - bearer: [] - summary: Authorize user-id to role mappings for JIT access - tags: - - Database - x-badges: - - name: 'OAuth scope: database:read' - position: after - x-endpoint-owners: - - security - x-fga-permissions: - - - database_jit_read - x-oauth-scope: database:read - put: - description: Modifies the roles that can be assumed and for how long - operationId: v1-update-jit-access - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateJitAccessBody' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/JitAccessResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to update JIT access - security: - - bearer: [] - summary: Updates a user mapping for JIT access - tags: - - Database - x-endpoint-owners: - - security - x-fga-permissions: - - - database_jit_write - /v1/projects/{ref}/database/jit/list: - get: - description: Mappings of roles a user can assume in the project database - operationId: v1-list-jit-access - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/JitListAccessResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to list database jit access - security: - - bearer: [] - summary: List all user-id to role mappings for JIT access - tags: - - Database - x-endpoint-owners: - - security - x-fga-permissions: - - - database_jit_write - /v1/projects/{ref}/database/jit/invite: - post: - description: >- - Invites the external user and sets initial roles that can be assumed and - for how long - operationId: v1-invite-external-jit-access - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/InviteExternalUserJitAccessBody' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/InviteExternalUserJitResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to invite external user - security: - - bearer: [] - summary: Invites an external user to a database for JIT access - tags: - - Database - x-endpoint-owners: - - security - x-fga-permissions: - - - database_jit_write - /v1/projects/{ref}/database/jit/invite/accept: - post: - description: Accepts the invitation to JIT database access - operationId: v1-accept-invite-external-jit-access - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/AcceptInviteExternalUserJitAccessBody' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/JitAccessResponse_Output' - '500': - description: Failed to accept invitation - security: - - bearer: [] - summary: Accepts invitation for JIT database access - tags: - - Database - x-endpoint-owners: - - security - /v1/projects/{ref}/database/jit/invite/{invite_id}: - delete: - description: Revokes and deletes the invitation - operationId: v1-delete-invite-external-jit-access - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: invite_id - required: true - in: path - schema: - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - example: 55555555-5555-4555-8555-555555555555 - type: string - responses: - '200': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to revoke invite for external user - security: - - bearer: [] - summary: Deletes the invite for an external user to a database for JIT access - tags: - - Database - x-endpoint-owners: - - security - x-fga-permissions: - - - database_jit_write - /v1/projects/{ref}/database/jit/{user_id}: - delete: - description: Remove JIT mappings of a user, revoking all JIT database access - operationId: v1-delete-jit-access - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: user_id - required: true - in: path - schema: - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - example: 55555555-5555-4555-8555-555555555555 - type: string - responses: - '200': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to remove JIT access - security: - - bearer: [] - summary: Delete JIT access by user-id - tags: - - Database - x-endpoint-owners: - - security - x-fga-permissions: - - - database_jit_write - /v1/projects/{ref}/database/openapi: - get: - description: >- - Returns the PostgREST OpenAPI specification for the project. This is the - replacement for querying `/rest/v1/` directly with the anon key. - operationId: v1-get-database-openapi - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: schema - required: false - in: query - description: The database schema to generate the OpenAPI spec for - schema: - default: public - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - type: object - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to fetch PostgREST OpenAPI spec - security: - - bearer: [] - summary: Get PostgREST OpenAPI spec - tags: - - Database - x-badges: - - name: 'OAuth scope: database:read' - position: after - x-endpoint-owners: - - control-plane - x-fga-permissions: - - - database_read - x-oauth-scope: database:read - /v1/projects/{ref}/functions: - get: - description: Returns all functions you've previously added to the specified project. - operationId: v1-list-all-functions - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/FunctionResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve project's functions - security: - - bearer: [] - summary: List all functions - tags: - - Edge Functions - x-badges: - - name: 'OAuth scope: edge_functions:read' - position: after - x-endpoint-owners: - - functions - x-fga-permissions: - - - edge_functions_read - x-oauth-scope: edge_functions:read - post: - deprecated: true - description: >- - This endpoint is deprecated - use the deploy endpoint. Creates a - function and adds it to the specified project. - operationId: v1-create-a-function - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: slug - required: false - in: query - schema: - pattern: ^[A-Za-z0-9_-]+$ - example: hello-world - type: string - - name: name - required: false - in: query - schema: - example: Hello World - type: string - - name: verify_jwt - required: false - in: query - schema: - example: true - type: string - - name: import_map - required: false - in: query - schema: - example: false - type: string - - name: entrypoint_path - required: false - in: query - schema: - example: index.ts - type: string - - name: import_map_path - required: false - in: query - schema: - example: import_map.json - type: string - - name: ezbr_sha256 - required: false - in: query - schema: - example: 44c691990518d25498f0fd80cf6631ecf2b58eb9c5eb2a087dd1688f2904dac7 - type: string - requestBody: - required: true - content: - application/vnd.denoland.eszip: - schema: - type: string - format: binary - application/json: - schema: - $ref: '#/components/schemas/V1CreateFunctionBody' - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/FunctionResponse_Output' - '401': - description: Unauthorized - '402': - description: Maximum number of functions reached for Plan - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to create project's function - security: - - bearer: [] - summary: Create a function - tags: - - Edge Functions - x-badges: - - name: 'OAuth scope: edge_functions:write' - position: after - x-endpoint-owners: - - functions - x-fga-permissions: - - - edge_functions_write - x-oauth-scope: edge_functions:write - put: - description: >- - Bulk update functions. It will create a new function or replace - existing. The operation is idempotent. NOTE: You will need to manually - bump the version. - operationId: v1-bulk-update-functions - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/BulkUpdateFunctionBody' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/BulkUpdateFunctionResponse_Output' - '401': - description: Unauthorized - '402': - description: Maximum number of functions reached for Plan - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to update functions - security: - - bearer: [] - summary: Bulk update functions - tags: - - Edge Functions - x-badges: - - name: 'OAuth scope: edge_functions:write' - position: after - x-endpoint-owners: - - functions - x-fga-permissions: - - - edge_functions_write - x-oauth-scope: edge_functions:write - /v1/projects/{ref}/functions/deploy: - post: - description: >- - A new endpoint to deploy functions. It will create if function does not - exist. - operationId: v1-deploy-a-function - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: slug - required: false - in: query - schema: - pattern: ^[A-Za-z][A-Za-z0-9_-]*$ - example: hello-world - type: string - - name: bundleOnly - required: false - in: query - schema: - example: false - type: string - requestBody: - required: true - content: - multipart/form-data: - schema: - $ref: '#/components/schemas/FunctionDeployBody' - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/DeployFunctionResponse_Output' - '401': - description: Unauthorized - '402': - description: Maximum number of functions reached for Plan - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to deploy function - security: - - bearer: [] - summary: Deploy a function - tags: - - Edge Functions - x-badges: - - name: 'OAuth scope: edge_functions:write' - position: after - x-endpoint-owners: - - functions - x-fga-permissions: - - - edge_functions_write - x-oauth-scope: edge_functions:write - /v1/projects/{ref}/functions/{function_slug}: - get: - description: Retrieves a function with the specified slug and project. - operationId: v1-get-a-function - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: function_slug - required: true - in: path - description: Function slug - schema: - pattern: ^[A-Za-z0-9_-]+$ - example: hello-world - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/FunctionSlugResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve function with given slug - security: - - bearer: [] - summary: Retrieve a function - tags: - - Edge Functions - x-badges: - - name: 'OAuth scope: edge_functions:read' - position: after - x-endpoint-owners: - - functions - x-fga-permissions: - - - edge_functions_read - x-oauth-scope: edge_functions:read - patch: - description: Updates a function with the specified slug and project. - operationId: v1-update-a-function - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: function_slug - required: true - in: path - description: Function slug - schema: - pattern: ^[A-Za-z0-9_-]+$ - example: hello-world - type: string - - name: slug - required: false - in: query - schema: - pattern: ^[A-Za-z0-9_-]+$ - example: hello-world - type: string - - name: name - required: false - in: query - schema: - example: Hello World - type: string - - name: verify_jwt - required: false - in: query - schema: - example: true - type: string - - name: import_map - required: false - in: query - schema: - example: false - type: string - - name: entrypoint_path - required: false - in: query - schema: - example: index.ts - type: string - - name: import_map_path - required: false - in: query - schema: - example: import_map.json - type: string - - name: ezbr_sha256 - required: false - in: query - schema: - example: 44c691990518d25498f0fd80cf6631ecf2b58eb9c5eb2a087dd1688f2904dac7 - type: string - requestBody: - required: true - content: - application/vnd.denoland.eszip: - schema: - $ref: '#/components/schemas/V1UpdateFunctionBodyEszip' - application/json: - schema: - $ref: '#/components/schemas/V1UpdateFunctionBodyJson' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/FunctionSlugResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to update function with given slug - security: - - bearer: [] - summary: Update a function - tags: - - Edge Functions - x-badges: - - name: 'OAuth scope: edge_functions:write' - position: after - x-endpoint-owners: - - functions - x-fga-permissions: - - - edge_functions_write - x-oauth-scope: edge_functions:write - delete: - description: Deletes a function with the specified slug from the specified project. - operationId: v1-delete-a-function - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: function_slug - required: true - in: path - description: Function slug - schema: - pattern: ^[A-Za-z0-9_-]+$ - example: hello-world - type: string - responses: - '200': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to delete function with given slug - security: - - bearer: [] - summary: Delete a function - tags: - - Edge Functions - x-badges: - - name: 'OAuth scope: edge_functions:write' - position: after - x-endpoint-owners: - - functions - x-fga-permissions: - - - edge_functions_write - x-oauth-scope: edge_functions:write - /v1/projects/{ref}/functions/{function_slug}/body: - get: - description: Retrieves a function body for the specified slug and project. - operationId: v1-get-a-function-body - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: function_slug - required: true - in: path - description: Function slug - schema: - pattern: ^[A-Za-z0-9_-]+$ - example: hello-world - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/StreamableFile' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve function body with given slug - security: - - bearer: [] - summary: Retrieve a function body - tags: - - Edge Functions - x-badges: - - name: 'OAuth scope: edge_functions:read' - position: after - x-endpoint-owners: - - functions - x-fga-permissions: - - - edge_functions_read - x-oauth-scope: edge_functions:read - /v1/projects/{ref}/storage/buckets: - get: - operationId: v1-list-all-buckets - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/V1StorageBucketResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to get list of buckets - security: - - bearer: [] - summary: Lists all buckets - tags: - - Storage - x-badges: - - name: 'OAuth scope: storage:read' - position: after - x-endpoint-owners: - - storage - x-fga-permissions: - - - storage_read - x-oauth-scope: storage:read - /v1/projects/{ref}/config/disk: - get: - operationId: v1-get-database-disk - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/DiskResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to get database disk attributes - security: - - bearer: [] - summary: Get database disk attributes - tags: - - Projects - x-endpoint-owners: - - control-plane - - infra - x-fga-permissions: - - - infra_disk_config_read - post: - operationId: v1-modify-database-disk - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/DiskRequestBody' - responses: - '201': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to modify database disk - security: - - bearer: [] - summary: Modify database disk - tags: - - Projects - x-endpoint-owners: - - control-plane - - infra - x-fga-permissions: - - - infra_disk_config_write - /v1/projects/{ref}/config/disk/util: - get: - operationId: v1-get-disk-utilization - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/DiskUtilMetricsResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to get disk utilization - security: - - bearer: [] - summary: Get disk utilization - tags: - - Projects - x-endpoint-owners: - - control-plane - - infra - x-fga-permissions: - - - infra_disk_config_read - /v1/projects/{ref}/config/disk/autoscale: - get: - operationId: v1-get-project-disk-autoscale-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/DiskAutoscaleConfig_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to get project disk autoscale config - security: - - bearer: [] - summary: Gets project disk autoscale config - tags: - - Projects - x-endpoint-owners: - - control-plane - - infra - x-fga-permissions: - - - infra_disk_config_read - /v1/projects/{ref}/config/storage: - get: - operationId: v1-get-storage-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/StorageConfigResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve project's storage config - security: - - bearer: [] - summary: Gets project's storage config - tags: - - Storage - x-endpoint-owners: - - storage - x-fga-permissions: - - - storage_config_read - patch: - operationId: v1-update-storage-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateStorageConfigBody' - responses: - '200': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to update project's storage config - security: - - bearer: [] - summary: Updates project's storage config - tags: - - Storage - x-endpoint-owners: - - storage - x-fga-permissions: - - - storage_config_write - /v1/projects/{ref}/config/database/pgbouncer: - get: - operationId: v1-get-project-pgbouncer-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/V1PgbouncerConfigResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve project's pgbouncer config - summary: Get project's pgbouncer config - tags: - - Database - x-badges: - - name: 'OAuth scope: database:read' - position: after - x-endpoint-owners: - - infra - - control-plane - x-fga-permissions: - - - database_read - x-oauth-scope: database:read - /v1/projects/{ref}/config/database/pooler: - get: - operationId: v1-get-pooler-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/SupavisorConfigResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve project's supavisor config - security: - - bearer: [] - summary: Gets project's supavisor config - tags: - - Database - x-badges: - - name: 'OAuth scope: database:read' - position: after - x-endpoint-owners: - - infra - x-fga-permissions: - - - database_pooling_config_read - x-oauth-scope: database:read - patch: - operationId: v1-update-pooler-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateSupavisorConfigBody' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateSupavisorConfigResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to update project's supavisor config - security: - - bearer: [] - summary: Updates project's supavisor config - tags: - - Database - x-badges: - - name: 'OAuth scope: database:write' - position: after - x-endpoint-owners: - - infra - x-fga-permissions: - - - database_pooling_config_write - x-oauth-scope: database:write - /v1/projects/{ref}/config/database/postgres: - get: - operationId: v1-get-postgres-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/PostgresConfigResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve project's Postgres config - security: - - bearer: [] - summary: Gets project's Postgres config - tags: - - Database - x-badges: - - name: 'OAuth scope: database:read' - position: after - x-endpoint-owners: - - infra - - control-plane - x-fga-permissions: - - - database_config_read - x-oauth-scope: database:read - put: - operationId: v1-update-postgres-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdatePostgresConfigBody' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/PostgresConfigResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to update project's Postgres config - security: - - bearer: [] - summary: Updates project's Postgres config - tags: - - Database - x-badges: - - name: 'OAuth scope: database:write' - position: after - x-endpoint-owners: - - infra - - control-plane - x-fga-permissions: - - - database_config_write - x-oauth-scope: database:write - /v1/projects/{ref}/config/realtime: - get: - operationId: v1-get-realtime-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: Gets project's realtime configuration - content: - application/json: - schema: - $ref: '#/components/schemas/RealtimeConfigResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Gets realtime configuration - tags: - - Realtime - x-endpoint-owners: - - realtime - x-fga-permissions: - - - realtime_config_read - patch: - operationId: v1-update-realtime-config - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateRealtimeConfigBody' - responses: - '204': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Updates realtime configuration - tags: - - Realtime - x-endpoint-owners: - - realtime - x-fga-permissions: - - - realtime_config_write - /v1/projects/{ref}/config/realtime/shutdown: - post: - operationId: v1-shutdown-realtime - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '204': - description: Realtime connections shutdown successfully - '401': - description: Unauthorized - '403': - description: Forbidden action - '404': - description: Tenant not found - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Shutdowns realtime connections for a project - tags: - - Realtime - x-endpoint-owners: - - realtime - x-fga-permissions: - - - realtime_config_write - /v1/projects/{ref}/config/auth/sso/providers: - post: - operationId: v1-create-a-sso-provider - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateProviderBody' - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/CreateProviderResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '404': - description: SAML 2.0 support is not enabled for this project - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Creates a new SSO provider - tags: - - Auth - x-badges: - - name: 'OAuth scope: auth:write' - position: after - x-endpoint-owners: - - auth - x-fga-permissions: - - - auth_config_write - x-oauth-scope: auth:write - get: - operationId: v1-list-all-sso-provider - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ListProvidersResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '404': - description: SAML 2.0 support is not enabled for this project - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Lists all SSO providers - tags: - - Auth - x-badges: - - name: 'OAuth scope: auth:read' - position: after - x-endpoint-owners: - - auth - x-fga-permissions: - - - auth_config_read - x-oauth-scope: auth:read - /v1/projects/{ref}/config/auth/sso/providers/{provider_id}: - get: - operationId: v1-get-a-sso-provider - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: provider_id - required: true - in: path - schema: - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - example: 77777777-7777-4777-8777-777777777777 - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/GetProviderResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '404': - description: >- - Either SAML 2.0 was not enabled for this project, or the provider - does not exist - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Gets a SSO provider by its UUID - tags: - - Auth - x-badges: - - name: 'OAuth scope: auth:read' - position: after - x-endpoint-owners: - - auth - x-fga-permissions: - - - auth_config_read - x-oauth-scope: auth:read - put: - operationId: v1-update-a-sso-provider - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: provider_id - required: true - in: path - schema: - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - example: 77777777-7777-4777-8777-777777777777 - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateProviderBody' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateProviderResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '404': - description: >- - Either SAML 2.0 was not enabled for this project, or the provider - does not exist - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Updates a SSO provider by its UUID - tags: - - Auth - x-badges: - - name: 'OAuth scope: auth:write' - position: after - x-endpoint-owners: - - auth - x-fga-permissions: - - - auth_config_write - x-oauth-scope: auth:write - delete: - operationId: v1-delete-a-sso-provider - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: provider_id - required: true - in: path - schema: - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - example: 77777777-7777-4777-8777-777777777777 - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/DeleteProviderResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '404': - description: >- - Either SAML 2.0 was not enabled for this project, or the provider - does not exist - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Removes a SSO provider by its UUID - tags: - - Auth - x-badges: - - name: 'OAuth scope: auth:write' - position: after - x-endpoint-owners: - - auth - x-fga-permissions: - - - auth_config_write - x-oauth-scope: auth:write - /v1/projects/{ref}/database/backups: - get: - operationId: v1-list-all-backups - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/V1BackupsResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to get backups - security: - - bearer: [] - summary: Lists all backups - tags: - - Database - x-badges: - - name: 'OAuth scope: database:read' - position: after - x-endpoint-owners: - - infra - x-fga-permissions: - - - backups_read - x-oauth-scope: database:read - /v1/projects/{ref}/database/backups/restore-pitr: - post: - operationId: v1-restore-pitr-backup - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/V1RestorePitrBody' - responses: - '201': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Restores a PITR backup for a database - tags: - - Database - x-badges: - - name: 'OAuth scope: database:write' - position: after - x-endpoint-owners: - - infra - x-fga-permissions: - - - backups_write - x-oauth-scope: database:write - /v1/projects/{ref}/database/backups/restore-point: - post: - operationId: v1-create-restore-point - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/V1RestorePointPostBody' - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/V1RestorePointResponse' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Initiates a creation of a restore point for a database - tags: - - Database - x-badges: - - name: 'OAuth scope: database:write' - position: after - x-endpoint-owners: - - infra - x-fga-permissions: - - - backups_write - x-internal: true - x-oauth-scope: database:write - get: - operationId: v1-get-restore-point - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - - name: name - required: false - in: query - schema: - maxLength: 20 - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/V1RestorePointResponse' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to get requested restore points - security: - - bearer: [] - summary: Get restore points for project - tags: - - Database - x-badges: - - name: 'OAuth scope: database:read' - position: after - x-endpoint-owners: - - infra - x-fga-permissions: - - - backups_read - x-internal: true - x-oauth-scope: database:read - /v1/projects/{ref}/database/backups/restore: - post: - operationId: v1-restore-physical-backup - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/V1RestoreBackupBody' - responses: - '201': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Restores a physical backup for a database - tags: - - Database - x-badges: - - name: 'OAuth scope: database:write' - position: after - x-endpoint-owners: - - infra - x-fga-permissions: - - - backups_write - x-internal: true - x-oauth-scope: database:write - /v1/projects/{ref}/database/backups/schedule: - get: - operationId: v1-get-backup-schedule - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/V1BackupScheduleResponse_Output' - '401': - description: Unauthorized - '402': - description: This feature requires the Enterprise organization plan. - content: - application/json: - schema: - $ref: '#/components/schemas/PlanGateErrorBody' - '403': - description: Forbidden action - '404': - description: Project or backup schedule not found - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve backup schedule - security: - - bearer: [] - summary: Gets the backup schedule for a project - tags: - - Database - x-allowed-plans: - - Enterprise - x-badges: - - name: 'OAuth scope: database:read' - position: after - - name: Only available on Enterprise - position: before - x-endpoint-owners: - - infra - x-fga-permissions: - - - backups_read - x-oauth-scope: database:read - patch: - description: >- - Sets the time at which the daily backup runs. The change takes effect on - the next backup window that includes the new time. If the new time has - already passed for today, the first backup at the new time will occur - the following day. It can only be updated 3 times per 24 hours. - operationId: v1-update-backup-schedule - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/V1UpdateBackupScheduleBody' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/V1BackupScheduleResponse_Output' - '400': - description: Invalid schedule_for format - '401': - description: Unauthorized - '402': - description: This feature requires the Enterprise organization plan. - content: - application/json: - schema: - $ref: '#/components/schemas/PlanGateErrorBody' - '403': - description: Forbidden action - '404': - description: Project or backup schedule not found - '429': - description: Rate limit exceeded - '500': - description: Failed to update backup schedule - security: - - bearer: [] - summary: Updates the backup schedule time for a project - tags: - - Database - x-allowed-plans: - - Enterprise - x-badges: - - name: 'OAuth scope: database:write' - position: after - - name: Only available on Enterprise - position: before - x-endpoint-owners: - - infra - x-fga-permissions: - - - backups_write - x-oauth-scope: database:write - /v1/projects/{ref}/database/backups/undo: - post: - operationId: v1-undo - parameters: - - name: ref - required: true - in: path - description: Project ref - schema: - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - example: abcdefghijklmnopqrst - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/V1UndoBody' - responses: - '201': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Initiates an undo to a given restore point - tags: - - Database - x-badges: - - name: 'OAuth scope: database:write' - position: after - x-endpoint-owners: - - infra - x-fga-permissions: - - - backups_write - x-internal: true - x-oauth-scope: database:write - /v1/organizations/{slug}/entitlements: - get: - description: >- - Returns the entitlements available to the organization based on their - plan and any overrides. - operationId: v1-get-organization-entitlements - parameters: - - name: slug - required: true - in: path - description: Organization slug - schema: - pattern: ^[\w-]+$ - example: tsrqponmlkjihgfedcba - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/V1ListEntitlementsResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Get entitlements for an organization - tags: - - Organizations - x-badges: - - name: 'OAuth scope: organizations:read' - position: after - x-endpoint-owners: - - billing - x-fga-permissions: - - - organization_admin_read - x-oauth-scope: organizations:read - /v1/organizations/{slug}/members: - get: - operationId: v1-list-organization-members - parameters: - - name: slug - required: true - in: path - description: Organization slug - schema: - pattern: ^[\w-]+$ - example: tsrqponmlkjihgfedcba - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/V1OrganizationMemberResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: List members of an organization - tags: - - Organizations - x-badges: - - name: 'OAuth scope: organizations:read' - position: after - x-endpoint-owners: - - control-plane - x-fga-permissions: - - - members_read - x-oauth-scope: organizations:read - /v1/organizations/{slug}: - get: - operationId: v1-get-an-organization - parameters: - - name: slug - required: true - in: path - description: Organization slug - schema: - pattern: ^[\w-]+$ - example: tsrqponmlkjihgfedcba - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/V1OrganizationSlugResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Gets information about the organization - tags: - - Organizations - x-badges: - - name: 'OAuth scope: organizations:read' - position: after - x-endpoint-owners: - - control-plane - x-fga-permissions: - - - organization_admin_read - x-oauth-scope: organizations:read - /v1/organizations/{slug}/project-claim/{token}: - get: - operationId: v1-get-organization-project-claim - parameters: - - name: slug - required: true - in: path - description: Organization slug - schema: - pattern: ^[\w-]+$ - example: tsrqponmlkjihgfedcba - type: string - - name: token - required: true - in: path - schema: - example: 0123456789abcdef0123456789abcdef01234567 - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/OrganizationProjectClaimResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Gets project details for the specified organization and claim token - tags: - - Organizations - x-endpoint-owners: - - control-plane - x-fga-permissions: - - - organization_admin_write - x-internal: true - post: - operationId: v1-claim-project-for-organization - parameters: - - name: slug - required: true - in: path - description: Organization slug - schema: - pattern: ^[\w-]+$ - example: tsrqponmlkjihgfedcba - type: string - - name: token - required: true - in: path - schema: - example: 0123456789abcdef0123456789abcdef01234567 - type: string - responses: - '204': - description: '' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - security: - - bearer: [] - summary: Claims project for the specified organization - tags: - - Organizations - x-endpoint-owners: - - control-plane - x-fga-permissions: - - - organization_admin_write - x-internal: true - /v1/organizations/{slug}/projects: - get: - description: >- - Returns a paginated list of projects for the specified organization. - - - This endpoint uses offset-based pagination. Use the `offset` parameter - to skip a number of projects and the `limit` parameter to control the - number of projects returned per page. - operationId: v1-get-all-projects-for-organization - parameters: - - name: slug - required: true - in: path - description: Organization slug - schema: - pattern: ^[\w-]+$ - example: tsrqponmlkjihgfedcba - type: string - - name: offset - required: false - in: query - description: Number of projects to skip - schema: - minimum: 0 - maximum: 9007199254740991 - default: 0 - example: 0 - type: integer - - name: limit - required: false - in: query - description: Number of projects to return per page - schema: - minimum: 1 - maximum: 100 - default: 100 - example: 20 - type: integer - - name: search - required: false - in: query - description: Search projects by name - schema: - example: acme - type: string - - name: sort - required: false - in: query - description: Sort order for projects - schema: - default: name_asc - example: created_desc - type: string - enum: - - name_asc - - name_desc - - created_asc - - created_desc - - name: statuses - required: false - in: query - description: |- - A comma-separated list of project statuses to filter by. - - The following values are supported: `ACTIVE_HEALTHY`, `INACTIVE`. - schema: - example: ACTIVE_HEALTHY,INACTIVE - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/OrganizationProjectsResponse_Output' - '401': - description: Unauthorized - '403': - description: Forbidden action - '429': - description: Rate limit exceeded - '500': - description: Failed to retrieve projects - security: - - bearer: [] - summary: Gets all projects for the given organization - tags: - - Projects - x-badges: - - name: 'OAuth scope: projects:read' - position: after - x-endpoint-owners: - - control-plane - x-fga-permissions: - - - organization_projects_read - x-oauth-scope: projects:read -info: - title: Supabase API (v1) - description: >- - Supabase API generated from the OpenAPI specification.
Visit - [https://supabase.com/docs](https://supabase.com/docs) for a complete - documentation. - version: 1.0.0 - contact: {} -tags: - - name: Advisors - description: Advisors related endpoints - - name: Analytics - description: Analytics related endpoints - - name: Auth - description: Auth related endpoints - - name: Billing - description: Billing related endpoints - - name: Database - description: Database related endpoints - - name: Domains - description: Domains related endpoints - - name: Edge Functions - description: Edge related endpoints - - name: Environments - description: Environments related endpoints - - name: OAuth - description: OAuth related endpoints - - name: Organizations - description: Organizations related endpoints - - name: Projects - description: Projects related endpoints - - name: Rest - description: Rest related endpoints - - name: Secrets - description: Secrets related endpoints - - name: Storage - description: >- - Visit - [https://supabase.github.io/storage/](https://supabase.github.io/storage/) - for complete documentation. -servers: - - url: https://api.supabase.green -components: - securitySchemes: - bearer: - scheme: bearer - bearerFormat: JWT - type: http - oauth2: - type: oauth2 - flows: - authorizationCode: - authorizationUrl: https://api.supabase.green/v1/oauth/authorize - tokenUrl: https://api.supabase.green/v1/oauth/token - scopes: - analytics:read: Read access to analytics - analytics:write: Write access to analytics - analytics_config:read: Read access to analytics config - analytics_config:write: Write access to analytics config - auth:read: Read access to auth - auth:write: Write access to auth - database:read: Read access to database - database:write: Write access to database - domains:read: Read access to domains - domains:write: Write access to domains - edge_functions:read: Read access to edge functions - edge_functions:write: Write access to edge functions - environment:read: Read access to environment - environment:write: Write access to environment - organizations:read: Read access to organizations - organizations:write: Write access to organizations - projects:read: Read access to projects - projects:write: Write access to projects - rest:read: Read access to rest - rest:write: Write access to rest - secrets:read: Read access to secrets - secrets:write: Write access to secrets - storage:read: Read access to storage - storage:write: Write access to storage - description: >- - OAuth 2.0 authorization code flow for Supabase OAuth apps. Tokens carry - the scopes approved for the app by the organization. - schemas: - BranchDetailResponse_Output: - type: object - properties: - ref: - type: string - postgres_version: - type: string - postgres_engine: - type: string - release_channel: - type: string - status: - type: string - enum: - - INACTIVE - - ACTIVE_HEALTHY - - ACTIVE_UNHEALTHY - - COMING_UP - - UNKNOWN - - GOING_DOWN - - INIT_FAILED - - REMOVED - - RESTORING - - UPGRADING - - PAUSING - - RESTORE_FAILED - - RESTARTING - - PAUSE_FAILED - - RESIZING - db_host: - type: string - db_port: - type: integer - exclusiveMinimum: true - maximum: 9007199254740991 - minimum: 0 - db_user: - type: string - db_pass: - type: string - jwt_secret: - type: string - required: - - ref - - postgres_version - - postgres_engine - - release_channel - - status - - db_host - - db_port - UpdateBranchBody: - type: object - properties: - branch_name: - type: string - git_branch: - type: string - reset_on_push: - description: >- - This field is deprecated and will be ignored. Use v1-reset-a-branch - endpoint directly instead. - deprecated: true - type: boolean - persistent: - type: boolean - status: - type: string - enum: - - CREATING_PROJECT - - RUNNING_MIGRATIONS - - MIGRATIONS_PASSED - - MIGRATIONS_FAILED - - FUNCTIONS_DEPLOYED - - FUNCTIONS_FAILED - request_review: - type: boolean - notify_url: - type: string - format: uri - description: HTTP endpoint to receive branch status updates. - example: - branch_name: preview-login-page - git_branch: feature/login-page - persistent: true - request_review: true - notify_url: https://example.com/webhooks/branches - BranchResponse_Output: - type: object - properties: - id: - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - name: - type: string - project_ref: - type: string - parent_project_ref: - type: string - is_default: - type: boolean - git_branch: - type: string - pr_number: - type: integer - format: int32 - minimum: -9007199254740991 - maximum: 9007199254740991 - latest_check_run_id: - description: This field is deprecated and will not be populated. - deprecated: true - type: number - persistent: - type: boolean - status: - type: string - enum: - - CREATING_PROJECT - - RUNNING_MIGRATIONS - - MIGRATIONS_PASSED - - MIGRATIONS_FAILED - - FUNCTIONS_DEPLOYED - - FUNCTIONS_FAILED - description: >- - This field is deprecated. List action runs to get branch status - instead. - deprecated: true - created_at: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - updated_at: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - review_requested_at: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - with_data: - type: boolean - notify_url: - type: string - format: uri - deletion_scheduled_at: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - preview_project_status: - type: string - enum: - - INACTIVE - - ACTIVE_HEALTHY - - ACTIVE_UNHEALTHY - - COMING_UP - - UNKNOWN - - GOING_DOWN - - INIT_FAILED - - REMOVED - - RESTORING - - UPGRADING - - PAUSING - - RESTORE_FAILED - - RESTARTING - - PAUSE_FAILED - - RESIZING - required: - - id - - name - - project_ref - - parent_project_ref - - is_default - - persistent - - status - - created_at - - updated_at - - with_data - BranchDeleteResponse_Output: - type: object - properties: - message: - type: string - enum: - - ok - required: - - message - BranchActionBody: - type: object - properties: - migration_version: - type: string - example: - migration_version: '20250312000000' - BranchUpdateResponse_Output: - type: object - properties: - workflow_run_id: - type: string - message: - type: string - enum: - - ok - required: - - workflow_run_id - - message - BranchRestoreResponse_Output: - type: object - properties: - message: - type: string - enum: - - Branch restoration initiated - required: - - message - V1ProjectWithDatabaseResponse_Output: - type: object - properties: - id: - type: string - deprecated: true - description: 'Deprecated: Use `ref` instead.' - ref: - type: string - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - description: Project ref - example: abcdefghijklmnopqrst - organization_id: - type: string - description: 'Deprecated: Use `organization_slug` instead.' - deprecated: true - organization_slug: - type: string - pattern: ^[\w-]+$ - description: Organization slug - example: tsrqponmlkjihgfedcba - name: - type: string - description: Name of your project - region: - type: string - description: Region of your project - created_at: - type: string - description: Creation timestamp - status: - type: string - enum: - - INACTIVE - - ACTIVE_HEALTHY - - ACTIVE_UNHEALTHY - - COMING_UP - - UNKNOWN - - GOING_DOWN - - INIT_FAILED - - REMOVED - - RESTORING - - UPGRADING - - PAUSING - - RESTORE_FAILED - - RESTARTING - - PAUSE_FAILED - - RESIZING - database: - type: object - properties: - host: - type: string - description: Database host - version: - type: string - description: Database version - postgres_engine: - type: string - description: Database engine - release_channel: - type: string - description: Release channel - required: - - host - - version - - postgres_engine - - release_channel - required: - - id - - ref - - organization_id - - organization_slug - - name - - region - - created_at - - status - - database - V1CreateProjectBody: - type: object - properties: - db_pass: - type: string - description: Database password - name: - type: string - maxLength: 256 - description: Name of your project - organization_id: - deprecated: true - description: 'Deprecated: Use `organization_slug` instead.' - type: string - organization_slug: - type: string - pattern: ^[\w-]+$ - description: Organization slug - example: tsrqponmlkjihgfedcba - plan: - deprecated: true - description: >- - Subscription Plan is now set on organization level and is ignored in - this request - type: string - enum: - - free - - pro - region: - description: >- - Region you want your server to reside in. Use region_selection - instead. - deprecated: true - enum: - - us-east-1 - - us-east-2 - - us-west-1 - - us-west-2 - - ap-east-1 - - ap-southeast-1 - - ap-northeast-1 - - ap-northeast-2 - - ap-southeast-2 - - eu-west-1 - - eu-west-2 - - eu-west-3 - - eu-north-1 - - eu-central-1 - - eu-central-2 - - ca-central-1 - - ap-south-1 - - sa-east-1 - type: string - region_selection: - description: >- - Region selection. Only one of region or region_selection can be - specified. - oneOf: - - type: object - properties: - type: - type: string - enum: - - specific - code: - type: string - description: >- - Specific region code. The codes supported are not a stable - API, and should be retrieved from the /available-regions - endpoint. - enum: - - us-east-1 - - us-east-2 - - us-west-1 - - us-west-2 - - ap-east-1 - - ap-southeast-1 - - ap-northeast-1 - - ap-northeast-2 - - ap-southeast-2 - - eu-west-1 - - eu-west-2 - - eu-west-3 - - eu-north-1 - - eu-central-1 - - eu-central-2 - - ca-central-1 - - ap-south-1 - - sa-east-1 - required: - - type - - code - - type: object - properties: - type: - type: string - enum: - - smartGroup - code: - type: string - enum: - - americas - - emea - - apac - description: >- - The Smart Region Group's code. The codes supported are not a - stable API, and should be retrieved from the - /available-regions endpoint. - required: - - type - - code - kps_enabled: - deprecated: true - description: This field is deprecated and is ignored in this request - type: boolean - desired_instance_size: - description: >- - Desired instance size. Omit this field to always default to the - smallest possible size. - type: string - enum: - - nano - - micro - - small - - medium - - large - - xlarge - - 2xlarge - - 4xlarge - - 8xlarge - - 12xlarge - - 16xlarge - - 24xlarge - - 24xlarge_optimized_memory - - 24xlarge_optimized_cpu - - 24xlarge_high_memory - - 48xlarge - - 48xlarge_optimized_memory - - 48xlarge_optimized_cpu - - 48xlarge_high_memory - template_url: - description: Template URL used to create the project from the CLI. - type: string - format: uri - release_channel: - deprecated: true - type: 'null' - postgres_engine: - deprecated: true - type: 'null' - high_availability: - description: '[Experimental] Whether to enable high availability for the project.' - type: boolean - required: - - db_pass - - name - - organization_slug - example: - db_pass: correct-horse-battery-staple - name: acme-prod - organization_slug: tsrqponmlkjihgfedcba - region: us-east-1 - additionalProperties: false - V1ProjectResponse_Output: - type: object - properties: - id: - type: string - deprecated: true - description: 'Deprecated: Use `ref` instead.' - ref: - type: string - minLength: 20 - maxLength: 20 - pattern: ^[a-z]+$ - description: Project ref - example: abcdefghijklmnopqrst - organization_id: - type: string - description: 'Deprecated: Use `organization_slug` instead.' - deprecated: true - organization_slug: - type: string - pattern: ^[\w-]+$ - description: Organization slug - example: tsrqponmlkjihgfedcba - name: - type: string - description: Name of your project - region: - type: string - description: Region of your project - created_at: - type: string - description: Creation timestamp - status: - type: string - enum: - - INACTIVE - - ACTIVE_HEALTHY - - ACTIVE_UNHEALTHY - - COMING_UP - - UNKNOWN - - GOING_DOWN - - INIT_FAILED - - REMOVED - - RESTORING - - UPGRADING - - PAUSING - - RESTORE_FAILED - - RESTARTING - - PAUSE_FAILED - - RESIZING - required: - - id - - ref - - organization_id - - organization_slug - - name - - region - - created_at - - status - RegionsInfoV1_Output: - type: object - properties: - recommendations: - type: object - properties: - smartGroup: - type: object - properties: - name: - type: string - code: - type: string - enum: - - americas - - emea - - apac - type: - type: string - enum: - - smartGroup - required: - - name - - code - - type - specific: - type: array - items: - type: object - properties: - name: - type: string - code: - type: string - enum: - - us-east-1 - - us-east-2 - - us-west-1 - - us-west-2 - - ap-southeast-1 - - ap-northeast-1 - - ap-northeast-2 - - ap-east-1 - - ap-southeast-2 - - eu-west-1 - - eu-west-2 - - eu-west-3 - - eu-north-1 - - eu-central-1 - - eu-central-2 - - ca-central-1 - - ap-south-1 - - sa-east-1 - type: - type: string - enum: - - specific - provider: - type: string - enum: - - AWS - - AWS_K8S - - AWS_NIMBUS - status: - type: string - enum: - - capacity - - other - required: - - name - - code - - type - - provider - required: - - smartGroup - - specific - all: - type: object - properties: - smartGroup: - type: array - items: - type: object - properties: - name: - type: string - code: - type: string - enum: - - americas - - emea - - apac - type: - type: string - enum: - - smartGroup - required: - - name - - code - - type - specific: - type: array - items: - type: object - properties: - name: - type: string - code: - type: string - enum: - - us-east-1 - - us-east-2 - - us-west-1 - - us-west-2 - - ap-southeast-1 - - ap-northeast-1 - - ap-northeast-2 - - ap-east-1 - - ap-southeast-2 - - eu-west-1 - - eu-west-2 - - eu-west-3 - - eu-north-1 - - eu-central-1 - - eu-central-2 - - ca-central-1 - - ap-south-1 - - sa-east-1 - type: - type: string - enum: - - specific - provider: - type: string - enum: - - AWS - - AWS_K8S - - AWS_NIMBUS - status: - type: string - enum: - - capacity - - other - required: - - name - - code - - type - - provider - required: - - smartGroup - - specific - required: - - recommendations - - all - OrganizationResponseV1_Output: - type: object - properties: - id: - type: string - description: 'Deprecated: Use `slug` instead.' - deprecated: true - slug: - type: string - pattern: ^[\w-]+$ - description: Organization slug - example: tsrqponmlkjihgfedcba - name: - type: string - required: - - id - - slug - - name - CreateOrganizationV1: - type: object - properties: - name: - type: string - maxLength: 256 - required: - - name - example: - name: Acme - additionalProperties: false - OAuthTokenBody: - type: object - properties: - grant_type: - type: string - enum: - - authorization_code - - refresh_token - - urn:ietf:params:oauth:grant-type:jwt-bearer - client_id: - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - client_secret: - type: string - code: - type: string - code_verifier: - type: string - redirect_uri: - type: string - refresh_token: - type: string - assertion: - description: >- - IDJAG assertion JWT for - grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer. Beta - - available on Team and Enterprise plans only. - type: string - resource: - description: Resource indicator for MCP (Model Context Protocol) clients - type: string - format: uri - scope: - type: string - example: - grant_type: authorization_code - client_id: 66666666-6666-4666-8666-666666666666 - client_secret: sb_secret_live_example_9f4d3a206b2e4a7e8c91 - code: oauth_code_9f4d3a206b2e4a7e8c91 - code_verifier: qW0Z6d9pQnW0mL1dK1q9wFq6Yz2nV5rA8jT3mP7sH4c - redirect_uri: https://app.acme.com/auth/callback - scope: projects:read projects:write - additionalProperties: false - OAuthTokenResponse_Output: - type: object - properties: - access_token: - type: string - refresh_token: - description: >- - The `urn:ietf:params:oauth:grant-type:jwt-bearer` grant type issues - access tokens only, no refresh token is returned and the token - cannot be revoked via `/v1/oauth/revoke`. - type: string - expires_in: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - token_type: - type: string - enum: - - Bearer - required: - - access_token - - expires_in - - token_type - OAuthRevokeTokenBody: - type: object - properties: - client_id: - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - client_secret: - type: string - refresh_token: - type: string - required: - - client_id - - client_secret - - refresh_token - example: - client_id: 66666666-6666-4666-8666-666666666666 - client_secret: sb_secret_live_example_9f4d3a206b2e4a7e8c91 - refresh_token: oauth_refresh_9f4d3a206b2e4a7e8c91 - additionalProperties: false - SnippetList_Output: - type: object - properties: - data: - type: array - items: - type: object - properties: - id: - type: string - inserted_at: - type: string - updated_at: - type: string - type: - type: string - enum: - - sql - visibility: - type: string - enum: - - user - - project - - org - - public - name: - type: string - description: - type: string - nullable: true - project: - type: object - properties: - id: - type: number - name: - type: string - required: - - id - - name - owner: - type: object - properties: - id: - type: number - username: - type: string - required: - - id - - username - updated_by: - type: object - properties: - id: - type: number - username: - type: string - required: - - id - - username - favorite: - type: boolean - required: - - id - - inserted_at - - updated_at - - type - - visibility - - name - - description - - project - - owner - - updated_by - - favorite - cursor: - type: string - required: - - data - SnippetResponse_Output: - type: object - properties: - id: - type: string - inserted_at: - type: string - updated_at: - type: string - type: - type: string - enum: - - sql - visibility: - type: string - enum: - - user - - project - - org - - public - name: - type: string - description: - type: string - nullable: true - project: - type: object - properties: - id: - type: number - name: - type: string - required: - - id - - name - owner: - type: object - properties: - id: - type: number - username: - type: string - required: - - id - - username - updated_by: - type: object - properties: - id: - type: number - username: - type: string - required: - - id - - username - favorite: - type: boolean - content: - type: object - properties: - favorite: - deprecated: true - description: 'Deprecated: Rely on root-level favorite property instead.' - type: boolean - schema_version: - type: string - sql: - type: string - required: - - sql - required: - - id - - inserted_at - - updated_at - - type - - visibility - - name - - description - - project - - owner - - updated_by - - favorite - - content - V1ProfileResponse_Output: - type: object - properties: - gotrue_id: - type: string - primary_email: - type: string - username: - type: string - required: - - gotrue_id - - primary_email - - username - ListActionRunResponse_Output: - type: array - items: - type: object - properties: - id: - type: string - branch_id: - type: string - run_steps: - type: array - items: - type: object - properties: - name: - type: string - enum: - - clone - - pull - - health - - configure - - migrate - - seed - - deploy - status: - type: string - enum: - - CREATED - - DEAD - - EXITED - - PAUSED - - REMOVING - - RESTARTING - - RUNNING - created_at: - type: string - updated_at: - type: string - required: - - name - - status - - created_at - - updated_at - git_config: - nullable: true - workdir: - type: string - nullable: true - check_run_id: - type: number - nullable: true - created_at: - type: string - updated_at: - type: string - required: - - id - - branch_id - - run_steps - - workdir - - check_run_id - - created_at - - updated_at - ActionRunResponse_Output: - type: object - properties: - id: - type: string - branch_id: - type: string - run_steps: - type: array - items: - type: object - properties: - name: - type: string - enum: - - clone - - pull - - health - - configure - - migrate - - seed - - deploy - status: - type: string - enum: - - CREATED - - DEAD - - EXITED - - PAUSED - - REMOVING - - RESTARTING - - RUNNING - created_at: - type: string - updated_at: - type: string - required: - - name - - status - - created_at - - updated_at - git_config: - nullable: true - workdir: - type: string - nullable: true - check_run_id: - type: number - nullable: true - created_at: - type: string - updated_at: - type: string - required: - - id - - branch_id - - run_steps - - workdir - - check_run_id - - created_at - - updated_at - UpdateRunStatusBody: - type: object - properties: - clone: - type: string - enum: - - CREATED - - DEAD - - EXITED - - PAUSED - - REMOVING - - RESTARTING - - RUNNING - pull: - type: string - enum: - - CREATED - - DEAD - - EXITED - - PAUSED - - REMOVING - - RESTARTING - - RUNNING - health: - type: string - enum: - - CREATED - - DEAD - - EXITED - - PAUSED - - REMOVING - - RESTARTING - - RUNNING - configure: - type: string - enum: - - CREATED - - DEAD - - EXITED - - PAUSED - - REMOVING - - RESTARTING - - RUNNING - migrate: - type: string - enum: - - CREATED - - DEAD - - EXITED - - PAUSED - - REMOVING - - RESTARTING - - RUNNING - seed: - type: string - enum: - - CREATED - - DEAD - - EXITED - - PAUSED - - REMOVING - - RESTARTING - - RUNNING - deploy: - type: string - enum: - - CREATED - - DEAD - - EXITED - - PAUSED - - REMOVING - - RESTARTING - - RUNNING - example: - clone: RUNNING - configure: RUNNING - migrate: RUNNING - deploy: CREATED - UpdateRunStatusResponse_Output: - type: object - properties: - message: - type: string - enum: - - ok - required: - - message - ApiKeyResponse_Output: - type: object - properties: - api_key: - type: string - nullable: true - id: - type: string - nullable: true - type: - type: string - enum: - - legacy - - publishable - - secret - - null - nullable: true - prefix: - type: string - nullable: true - name: - type: string - description: - type: string - nullable: true - hash: - type: string - nullable: true - secret_jwt_template: - type: object - propertyNames: - type: string - additionalProperties: {} - nullable: true - inserted_at: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - nullable: true - updated_at: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - nullable: true - required: - - name - LegacyApiKeysResponse_Output: - type: object - properties: - enabled: - type: boolean - required: - - enabled - CreateApiKeyBody: - type: object - properties: - type: - type: string - enum: - - publishable - - secret - name: - type: string - minLength: 4 - maxLength: 64 - pattern: ^[a-z_][a-z0-9_]+$ - description: - type: string - nullable: true - secret_jwt_template: - type: object - propertyNames: - type: string - additionalProperties: {} - nullable: true - required: - - type - - name - example: - type: secret - name: ci_secret_key - description: CI deploy key - UpdateApiKeyBody: - type: object - properties: - name: - type: string - minLength: 4 - maxLength: 64 - pattern: ^[a-z_][a-z0-9_]+$ - description: - type: string - nullable: true - secret_jwt_template: - type: object - propertyNames: - type: string - additionalProperties: {} - nullable: true - example: - name: ci_secret_key_rotated - description: Rotated after March release - CreateBranchBody: - type: object - properties: - branch_name: - type: string - minLength: 1 - git_branch: - type: string - is_default: - type: boolean - persistent: - type: boolean - region: - type: string - desired_instance_size: - type: string - enum: - - pico - - nano - - micro - - small - - medium - - large - - xlarge - - 2xlarge - - 4xlarge - - 8xlarge - - 12xlarge - - 16xlarge - - 24xlarge - - 24xlarge_optimized_memory - - 24xlarge_optimized_cpu - - 24xlarge_high_memory - - 48xlarge - - 48xlarge_optimized_memory - - 48xlarge_optimized_cpu - - 48xlarge_high_memory - release_channel: - type: string - enum: - - internal - - alpha - - beta - - ga - - withdrawn - - preview - description: Release channel. If not provided, GA will be used. - postgres_engine: - type: string - enum: - - '15' - - '17' - - 17-oriole - description: >- - Postgres engine version. If not provided, the latest version will be - used. - secrets: - type: object - additionalProperties: - type: string - with_data: - type: boolean - notify_url: - type: string - format: uri - description: HTTP endpoint to receive branch status updates. - required: - - branch_name - example: - branch_name: preview-login-page - git_branch: feature/login-page - persistent: true - with_data: false - notify_url: https://example.com/webhooks/branches - JsonValue_Output: - description: Any JSON-serializable value - anyOf: - - anyOf: - - type: string - - type: number - - type: boolean - nullable: true - - type: array - items: - $ref: '#/components/schemas/JsonValue_Output' - - type: object - additionalProperties: - $ref: '#/components/schemas/JsonValue_Output' - UpdateCustomHostnameResponse_Output: - type: object - properties: - status: - type: string - enum: - - 1_not_started - - 2_initiated - - 3_challenge_verified - - 4_origin_setup_completed - - 5_services_reconfigured - custom_hostname: - type: string - data: - type: object - properties: - success: - type: boolean - errors: - type: array - items: - $ref: '#/components/schemas/JsonValue_Output' - messages: - type: array - items: - $ref: '#/components/schemas/JsonValue_Output' - result: - type: object - properties: - id: - type: string - hostname: - type: string - ssl: - type: object - properties: - status: - type: string - validation_records: - type: array - items: - type: object - properties: - txt_name: - type: string - txt_value: - type: string - validation_errors: - type: array - items: - type: object - properties: - message: - type: string - required: - - message - ownership_verification: - type: object - properties: - type: - type: string - name: - type: string - value: - type: string - custom_origin_server: - type: string - verification_errors: - type: array - items: - type: string - status: - type: string - required: - - id - - hostname - required: - - success - - errors - - messages - - result - required: - - status - - data - UpdateCustomHostnameBody: - type: object - properties: - custom_hostname: - type: string - minLength: 1 - maxLength: 253 - required: - - custom_hostname - example: - custom_hostname: docs.example.com - JitAccessRequestRequest: - type: object - properties: - state: - type: string - enum: - - enabled - - disabled - required: - - state - example: - state: enabled - NetworkBanResponse_Output: - type: object - properties: - banned_ipv4_addresses: - type: array - items: - type: string - required: - - banned_ipv4_addresses - NetworkBanResponseEnriched_Output: - type: object - properties: - banned_ipv4_addresses: - type: array - items: - type: object - properties: - banned_address: - type: string - identifier: - type: string - type: - type: string - required: - - banned_address - - identifier - - type - required: - - banned_ipv4_addresses - RemoveNetworkBanRequest: - type: object - properties: - ipv4_addresses: - type: array - items: - type: string - description: List of IP addresses to unban. - requester_ip: - default: false - description: Include requester's public IP in the list of addresses to unban. - type: boolean - identifier: - type: string - required: - - ipv4_addresses - example: - ipv4_addresses: - - 203.0.113.10 - requester_ip: false - NetworkRestrictionsResponse_Output: - type: object - properties: - entitlement: - type: string - enum: - - disallowed - - allowed - config: - type: object - properties: - dbAllowedCidrs: - type: array - items: - type: string - dbAllowedCidrsV6: - type: array - items: - type: string - example: - dbAllowedCidrs: - - 203.0.113.0/24 - dbAllowedCidrsV6: - - 2001:db8::/32 - description: >- - At any given point in time, this is the config that the user has - requested be applied to their project. The `status` field indicates - if it has been applied to the project, or is pending. When an - updated config is received, the applied config is moved to - `old_config`. - old_config: - description: >- - Populated when a new config has been received, but not registered as - successfully applied to a project. - type: object - properties: - dbAllowedCidrs: - type: array - items: - type: string - dbAllowedCidrsV6: - type: array - items: - type: string - example: - dbAllowedCidrs: - - 203.0.113.0/24 - dbAllowedCidrsV6: - - 2001:db8::/32 - status: - type: string - enum: - - stored - - applied - updated_at: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - applied_at: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - required: - - entitlement - - config - - status - NetworkRestrictionsRequest: - type: object - properties: - dbAllowedCidrs: - type: array - items: - type: string - dbAllowedCidrsV6: - type: array - items: - type: string - example: - dbAllowedCidrs: - - 203.0.113.0/24 - dbAllowedCidrsV6: - - 2001:db8::/32 - NetworkRestrictionsPatchRequest: - type: object - properties: - add: - type: object - properties: - dbAllowedCidrs: - type: array - items: - type: string - dbAllowedCidrsV6: - type: array - items: - type: string - remove: - type: object - properties: - dbAllowedCidrs: - type: array - items: - type: string - dbAllowedCidrsV6: - type: array - items: - type: string - example: - add: - dbAllowedCidrs: - - 203.0.113.0/24 - remove: - dbAllowedCidrs: - - 198.51.100.0/24 - NetworkRestrictionsV2Response_Output: - type: object - properties: - entitlement: - type: string - enum: - - disallowed - - allowed - config: - type: object - properties: - dbAllowedCidrs: - type: array - items: - type: object - properties: - address: - type: string - type: - type: string - enum: - - v4 - - v6 - required: - - address - - type - description: >- - At any given point in time, this is the config that the user has - requested be applied to their project. The `status` field indicates - if it has been applied to the project, or is pending. When an - updated config is received, the applied config is moved to - `old_config`. - old_config: - description: >- - Populated when a new config has been received, but not registered as - successfully applied to a project. - type: object - properties: - dbAllowedCidrs: - type: array - items: - type: object - properties: - address: - type: string - type: - type: string - enum: - - v4 - - v6 - required: - - address - - type - updated_at: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - applied_at: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - status: - type: string - enum: - - stored - - applied - required: - - entitlement - - config - - status - PgsodiumConfigResponse_Output: - type: object - properties: - root_key: - type: string - description: 'The pgsodium root key: 32 bytes, hex-encoded (64 characters).' - required: - - root_key - example: - root_key: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef - UpdatePgsodiumConfigBody: - type: object - properties: - root_key: - type: string - description: 'The pgsodium root key: 32 bytes, hex-encoded (64 characters).' - required: - - root_key - example: - root_key: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef - PostgrestConfigWithJWTSecretResponse_Output: - type: object - properties: - db_schema: - type: string - max_rows: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - db_extra_search_path: - type: string - db_pool: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - description: >- - If `null`, the value is automatically configured based on compute - size. - nullable: true - db_pool_acquisition_timeout: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - description: If `null`, the value is automatically configured to 10. - nullable: true - db_pool_max_lifetime: - type: integer - minimum: 0 - maximum: 3600 - description: If `null`, the value is automatically configured to 1800. - nullable: true - db_pool_max_idletime: - type: integer - minimum: 0 - maximum: 60 - description: If `null`, the value is automatically configured to 30. - nullable: true - jwt_secret: - type: string - required: - - db_schema - - max_rows - - db_extra_search_path - - db_pool - - db_pool_acquisition_timeout - - db_pool_max_lifetime - - db_pool_max_idletime - V1UpdatePostgrestConfigBody: - type: object - properties: - db_extra_search_path: - type: string - db_schema: - type: string - max_rows: - type: integer - minimum: 0 - maximum: 1000000 - db_pool: - type: integer - minimum: 0 - maximum: 1000 - db_pool_acquisition_timeout: - type: integer - minimum: 0 - maximum: 60 - db_pool_max_lifetime: - type: integer - minimum: 0 - maximum: 3600 - db_pool_max_idletime: - type: integer - minimum: 0 - maximum: 60 - example: - db_schema: public,storage - db_pool: 20 - max_rows: 1000 - V1PostgrestConfigResponse_Output: - type: object - properties: - db_schema: - type: string - max_rows: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - db_extra_search_path: - type: string - db_pool: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - description: >- - If `null`, the value is automatically configured based on compute - size. - nullable: true - db_pool_acquisition_timeout: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - description: If `null`, the value is automatically configured to 10. - nullable: true - db_pool_max_lifetime: - type: integer - minimum: 0 - maximum: 3600 - description: If `null`, the value is automatically configured to 1800. - nullable: true - db_pool_max_idletime: - type: integer - minimum: 0 - maximum: 60 - description: If `null`, the value is automatically configured to 30. - nullable: true - required: - - db_schema - - max_rows - - db_extra_search_path - - db_pool - - db_pool_acquisition_timeout - - db_pool_max_lifetime - - db_pool_max_idletime - V1ProjectRefResponse_Output: - type: object - properties: - id: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - ref: - type: string - name: - type: string - required: - - id - - ref - - name - V1UpdateProjectBody: - type: object - properties: - name: - type: string - minLength: 1 - maxLength: 256 - required: - - name - example: - name: Acme Platform - SecretResponse_Output: - type: object - properties: - name: - type: string - value: - type: string - updated_at: - type: string - required: - - name - - value - CreateSecretBody: - maxItems: 100 - type: array - items: - type: object - properties: - name: - type: string - maxLength: 256 - pattern: ^(?!SUPABASE_).* - description: Secret name must not start with the SUPABASE_ prefix. - value: - type: string - maxLength: 24576 - required: - - name - - value - example: - - name: OPENAI_API_KEY - value: sk-example-secret - - name: STRIPE_WEBHOOK_SECRET - value: whsec_example - DeleteSecretsBody: - type: array - items: - type: string - example: - - OPENAI_API_KEY - SslEnforcementResponse_Output: - type: object - properties: - currentConfig: - type: object - properties: - database: - type: boolean - required: - - database - appliedSuccessfully: - type: boolean - required: - - currentConfig - - appliedSuccessfully - SslEnforcementRequest: - type: object - properties: - requestedConfig: - type: object - properties: - database: - type: boolean - required: - - database - required: - - requestedConfig - example: - requestedConfig: - database: true - TypescriptResponse_Output: - type: object - properties: - types: - type: string - required: - - types - VanitySubdomainConfigResponse_Output: - type: object - properties: - status: - type: string - enum: - - not-used - - custom-domain-used - - active - custom_domain: - type: string - minLength: 1 - required: - - status - PlanGateErrorBody: - type: object - properties: - message: - type: string - description: Human-readable explanation of the plan gate - error: - description: >- - Present on entitlement denials. Other errors with this status code - (validation, billing state) carry only message. - type: object - properties: - code: - type: string - description: Machine-readable marker for plan-gated denials - enum: - - entitlement_required - feature: - type: string - description: Entitlement feature key that failed the check - upgrade_url: - description: >- - Billing page URL for the organization, present when the org is - resolvable - type: string - required: - - code - - feature - required: - - message - VanitySubdomainBody: - type: object - properties: - vanity_subdomain: - type: string - maxLength: 63 - required: - - vanity_subdomain - example: - vanity_subdomain: acme-prod - SubdomainAvailabilityResponse_Output: - type: object - properties: - available: - type: boolean - required: - - available - ActivateVanitySubdomainResponse_Output: - type: object - properties: - custom_domain: - type: string - required: - - custom_domain - UpgradeDatabaseBody: - type: object - properties: - target_version: - type: string - release_channel: - type: string - enum: - - internal - - alpha - - beta - - ga - - withdrawn - - preview - required: - - target_version - example: - target_version: '17' - release_channel: ga - ProjectUpgradeInitiateResponse_Output: - type: object - properties: - tracking_id: - type: string - required: - - tracking_id - ProjectUpgradeEligibilityResponse_Output: - type: object - properties: - eligible: - type: boolean - current_app_version: - type: string - current_app_version_release_channel: - type: string - enum: - - internal - - alpha - - beta - - ga - - withdrawn - - preview - latest_app_version: - type: string - target_upgrade_versions: - type: array - items: - type: object - properties: - postgres_version: - type: string - enum: - - '13' - - '14' - - '15' - - '17' - - 17-oriole - release_channel: - type: string - enum: - - internal - - alpha - - beta - - ga - - withdrawn - - preview - app_version: - type: string - required: - - postgres_version - - release_channel - - app_version - duration_estimate_hours: - type: number - legacy_auth_custom_roles: - type: array - items: - type: string - objects_to_be_dropped: - type: array - items: - type: string - deprecated: true - description: Use validation_errors instead. - unsupported_extensions: - type: array - items: - type: string - deprecated: true - description: Use validation_errors instead. - user_defined_objects_in_internal_schemas: - type: array - items: - type: string - deprecated: true - description: Use validation_errors instead. - validation_errors: - type: array - items: - anyOf: - - type: object - properties: - type: - type: string - enum: - - objects_depending_on_pg_cron - dependents: - type: array - items: - type: string - required: - - type - - dependents - - type: object - properties: - type: - type: string - enum: - - indexes_referencing_ll_to_earth - schema_name: - type: string - table_name: - type: string - index_name: - type: string - required: - - type - - schema_name - - table_name - - index_name - - type: object - properties: - type: - type: string - enum: - - function_using_obsolete_lang - schema_name: - type: string - function_name: - type: string - lang_name: - type: string - required: - - type - - schema_name - - function_name - - lang_name - - type: object - properties: - type: - type: string - enum: - - unsupported_extension - extension_name: - type: string - required: - - type - - extension_name - - type: object - properties: - type: - type: string - enum: - - unsupported_fdw_handler - fdw_name: - type: string - fdw_handler_name: - type: string - required: - - type - - fdw_name - - fdw_handler_name - - type: object - properties: - type: - type: string - enum: - - unlogged_table_with_persistent_sequence - schema_name: - type: string - table_name: - type: string - sequence_name: - type: string - required: - - type - - schema_name - - table_name - - sequence_name - - type: object - properties: - type: - type: string - enum: - - user_defined_objects_in_internal_schemas - obj_type: - anyOf: - - type: string - enum: - - table - - type: string - enum: - - function - schema_name: - type: string - obj_name: - type: string - required: - - type - - obj_type - - schema_name - - obj_name - - type: object - properties: - type: - type: string - enum: - - active_replication_slot - slot_name: - type: string - required: - - type - - slot_name - - type: object - properties: - type: - type: string - enum: - - x86_architecture - required: - - type - - type: object - properties: - type: - type: string - enum: - - project_hibernating - required: - - type - warnings: - type: array - items: - oneOf: - - type: object - properties: - type: - type: string - enum: - - pg_graphql_introspection_change - required: - - type - - type: object - properties: - type: - type: string - enum: - - ltree_reindex_required - required: - - type - - type: object - properties: - type: - type: string - enum: - - operator_estimator_gate - required: - - type - - type: object - properties: - type: - type: string - enum: - - btree_gist_nan_reindex - required: - - type - required: - - eligible - - current_app_version - - current_app_version_release_channel - - latest_app_version - - target_upgrade_versions - - duration_estimate_hours - - legacy_auth_custom_roles - - objects_to_be_dropped - - unsupported_extensions - - user_defined_objects_in_internal_schemas - - validation_errors - - warnings - DatabaseUpgradeStatusResponse_Output: - type: object - properties: - databaseUpgradeStatus: - type: object - properties: - initiated_at: - type: string - latest_status_at: - type: string - target_version: - type: string - error: - type: string - enum: - - 1_upgraded_instance_launch_failed - - 2_volume_detachchment_from_upgraded_instance_failed - - 3_volume_attachment_to_original_instance_failed - - 4_data_upgrade_initiation_failed - - 5_data_upgrade_completion_failed - - 6_volume_detachchment_from_original_instance_failed - - 7_volume_attachment_to_upgraded_instance_failed - - 8_upgrade_completion_failed - - 9_post_physical_backup_failed - progress: - type: string - enum: - - 0_requested - - 1_started - - 2_launched_upgraded_instance - - 3_detached_volume_from_upgraded_instance - - 4_attached_volume_to_original_instance - - 5_initiated_data_upgrade - - 6_completed_data_upgrade - - 7_detached_volume_from_original_instance - - 8_attached_volume_to_upgraded_instance - - 9_completed_upgrade - - 10_completed_post_physical_backup - status: - type: number - required: - - initiated_at - - latest_status_at - - target_version - - status - nullable: true - required: - - databaseUpgradeStatus - ReadOnlyStatusResponse_Output: - type: object - properties: - enabled: - type: boolean - override_enabled: - type: boolean - override_active_until: - type: string - required: - - enabled - - override_enabled - - override_active_until - SetUpReadReplicaBody: - type: object - properties: - read_replica_region: - type: string - enum: - - us-east-1 - - us-east-2 - - us-west-1 - - us-west-2 - - ap-east-1 - - ap-southeast-1 - - ap-northeast-1 - - ap-northeast-2 - - ap-southeast-2 - - eu-west-1 - - eu-west-2 - - eu-west-3 - - eu-north-1 - - eu-central-1 - - eu-central-2 - - ca-central-1 - - ap-south-1 - - sa-east-1 - description: Region you want your read replica to reside in - required: - - read_replica_region - example: - read_replica_region: us-west-1 - RemoveReadReplicaBody: - type: object - properties: - database_identifier: - type: string - required: - - database_identifier - example: - database_identifier: abcdefghijklmnopqrst-rr-us-west-1-abcde - V1ServiceHealthResponse_Output: - type: object - properties: - name: - type: string - enum: - - auth - - db - - db_postgres_user - - pooler - - realtime - - rest - - storage - - pg_bouncer - healthy: - type: boolean - deprecated: true - description: Deprecated. Use `status` instead. - status: - type: string - enum: - - COMING_UP - - ACTIVE_HEALTHY - - UNHEALTHY - info: - anyOf: - - type: object - properties: - name: - type: string - enum: - - GoTrue - version: - type: string - description: - type: string - required: - - name - - version - - description - - type: object - properties: - healthy: - type: boolean - deprecated: true - description: Deprecated. Use `status` instead. - db_connected: - type: boolean - replication_connected: - type: boolean - connected_cluster: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - required: - - healthy - - db_connected - - replication_connected - - connected_cluster - - type: object - properties: - db_schema: - type: string - required: - - db_schema - error: - type: string - required: - - name - - healthy - - status - SigningKeyResponse_Output: - type: object - properties: - id: - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - algorithm: - type: string - enum: - - EdDSA - - ES256 - - RS256 - - HS256 - status: - type: string - enum: - - in_use - - previously_used - - revoked - - standby - public_jwk: - nullable: true - created_at: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - updated_at: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - required: - - id - - algorithm - - status - - public_jwk - - created_at - - updated_at - CreateSigningKeyBody: - type: object - properties: - algorithm: - type: string - enum: - - EdDSA - - ES256 - - RS256 - - HS256 - status: - type: string - enum: - - in_use - - standby - private_jwk: - oneOf: - - type: object - properties: - kid: - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - use: - type: string - enum: - - sig - key_ops: - minItems: 2 - maxItems: 2 - type: array - items: - type: string - enum: - - sign - - verify - ext: - type: boolean - enum: - - true - kty: - type: string - enum: - - RSA - alg: - type: string - enum: - - RS256 - 'n': - type: string - e: - type: string - enum: - - AQAB - d: - type: string - p: - type: string - q: - type: string - dp: - type: string - dq: - type: string - qi: - type: string - required: - - kty - - 'n' - - e - - d - - p - - q - - dp - - dq - - qi - additionalProperties: false - - type: object - properties: - kid: - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - use: - type: string - enum: - - sig - key_ops: - minItems: 2 - maxItems: 2 - type: array - items: - type: string - enum: - - sign - - verify - ext: - type: boolean - enum: - - true - kty: - type: string - enum: - - EC - alg: - type: string - enum: - - ES256 - crv: - type: string - enum: - - P-256 - x: - type: string - 'y': - type: string - d: - type: string - required: - - kty - - crv - - x - - 'y' - - d - additionalProperties: false - - type: object - properties: - kid: - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - use: - type: string - enum: - - sig - key_ops: - minItems: 2 - maxItems: 2 - type: array - items: - type: string - enum: - - sign - - verify - ext: - type: boolean - enum: - - true - kty: - type: string - enum: - - OKP - alg: - type: string - enum: - - EdDSA - crv: - type: string - enum: - - Ed25519 - x: - type: string - d: - type: string - required: - - kty - - crv - - x - - d - additionalProperties: false - - type: object - properties: - kid: - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - use: - type: string - enum: - - sig - key_ops: - minItems: 2 - maxItems: 2 - type: array - items: - type: string - enum: - - sign - - verify - ext: - type: boolean - enum: - - true - kty: - type: string - enum: - - oct - alg: - type: string - enum: - - HS256 - k: - type: string - minLength: 16 - required: - - kty - - k - additionalProperties: false - required: - - algorithm - example: - algorithm: RS256 - status: standby - additionalProperties: false - SigningKeysResponse_Output: - type: object - properties: - keys: - type: array - items: - type: object - properties: - id: - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - algorithm: - type: string - enum: - - EdDSA - - ES256 - - RS256 - - HS256 - status: - type: string - enum: - - in_use - - previously_used - - revoked - - standby - public_jwk: - nullable: true - created_at: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - updated_at: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - required: - - id - - algorithm - - status - - public_jwk - - created_at - - updated_at - required: - - keys - UpdateSigningKeyBody: - type: object - properties: - status: - type: string - enum: - - in_use - - previously_used - - revoked - - standby - required: - - status - example: - status: standby - additionalProperties: false - AuthConfigResponse_Output: - type: object - properties: - api_max_request_duration: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - db_max_pool_size: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - db_max_pool_size_unit: - type: string - enum: - - connections - - percent - - null - nullable: true - disable_signup: - type: boolean - nullable: true - external_anonymous_users_enabled: - type: boolean - nullable: true - external_apple_additional_client_ids: - type: string - nullable: true - external_apple_client_id: - type: string - nullable: true - external_apple_email_optional: - type: boolean - nullable: true - external_apple_enabled: - type: boolean - nullable: true - external_apple_secret: - type: string - nullable: true - external_azure_client_id: - type: string - nullable: true - external_azure_email_optional: - type: boolean - nullable: true - external_azure_enabled: - type: boolean - nullable: true - external_azure_secret: - type: string - nullable: true - external_azure_url: - type: string - nullable: true - external_bitbucket_client_id: - type: string - nullable: true - external_bitbucket_email_optional: - type: boolean - nullable: true - external_bitbucket_enabled: - type: boolean - nullable: true - external_bitbucket_secret: - type: string - nullable: true - external_discord_client_id: - type: string - nullable: true - external_discord_email_optional: - type: boolean - nullable: true - external_discord_enabled: - type: boolean - nullable: true - external_discord_secret: - type: string - nullable: true - external_email_enabled: - type: boolean - nullable: true - external_facebook_client_id: - type: string - nullable: true - external_facebook_email_optional: - type: boolean - nullable: true - external_facebook_enabled: - type: boolean - nullable: true - external_facebook_secret: - type: string - nullable: true - external_figma_client_id: - type: string - nullable: true - external_figma_email_optional: - type: boolean - nullable: true - external_figma_enabled: - type: boolean - nullable: true - external_figma_secret: - type: string - nullable: true - external_github_client_id: - type: string - nullable: true - external_github_email_optional: - type: boolean - nullable: true - external_github_enabled: - type: boolean - nullable: true - external_github_secret: - type: string - nullable: true - external_gitlab_client_id: - type: string - nullable: true - external_gitlab_email_optional: - type: boolean - nullable: true - external_gitlab_enabled: - type: boolean - nullable: true - external_gitlab_secret: - type: string - nullable: true - external_gitlab_url: - type: string - nullable: true - external_google_additional_client_ids: - type: string - nullable: true - external_google_client_id: - type: string - nullable: true - external_google_email_optional: - type: boolean - nullable: true - external_google_enabled: - type: boolean - nullable: true - external_google_secret: - type: string - nullable: true - external_google_skip_nonce_check: - type: boolean - nullable: true - external_kakao_client_id: - type: string - nullable: true - external_kakao_email_optional: - type: boolean - nullable: true - external_kakao_enabled: - type: boolean - nullable: true - external_kakao_secret: - type: string - nullable: true - external_keycloak_client_id: - type: string - nullable: true - external_keycloak_email_optional: - type: boolean - nullable: true - external_keycloak_enabled: - type: boolean - nullable: true - external_keycloak_secret: - type: string - nullable: true - external_keycloak_url: - type: string - nullable: true - external_linkedin_oidc_client_id: - type: string - nullable: true - external_linkedin_oidc_email_optional: - type: boolean - nullable: true - external_linkedin_oidc_enabled: - type: boolean - nullable: true - external_linkedin_oidc_secret: - type: string - nullable: true - external_slack_oidc_client_id: - type: string - nullable: true - external_slack_oidc_email_optional: - type: boolean - nullable: true - external_slack_oidc_enabled: - type: boolean - nullable: true - external_slack_oidc_secret: - type: string - nullable: true - external_notion_client_id: - type: string - nullable: true - external_notion_email_optional: - type: boolean - nullable: true - external_notion_enabled: - type: boolean - nullable: true - external_notion_secret: - type: string - nullable: true - external_phone_enabled: - type: boolean - nullable: true - external_slack_client_id: - type: string - nullable: true - external_slack_email_optional: - type: boolean - nullable: true - external_slack_enabled: - type: boolean - nullable: true - external_slack_secret: - type: string - nullable: true - external_spotify_client_id: - type: string - nullable: true - external_spotify_email_optional: - type: boolean - nullable: true - external_spotify_enabled: - type: boolean - nullable: true - external_spotify_secret: - type: string - nullable: true - external_twitch_client_id: - type: string - nullable: true - external_twitch_email_optional: - type: boolean - nullable: true - external_twitch_enabled: - type: boolean - nullable: true - external_twitch_secret: - type: string - nullable: true - external_twitter_client_id: - type: string - nullable: true - external_twitter_email_optional: - type: boolean - nullable: true - external_twitter_enabled: - type: boolean - nullable: true - external_twitter_secret: - type: string - nullable: true - external_x_client_id: - type: string - nullable: true - external_x_email_optional: - type: boolean - nullable: true - external_x_enabled: - type: boolean - nullable: true - external_x_secret: - type: string - nullable: true - external_workos_client_id: - type: string - nullable: true - external_workos_enabled: - type: boolean - nullable: true - external_workos_secret: - type: string - nullable: true - external_workos_url: - type: string - nullable: true - external_web3_solana_enabled: - type: boolean - nullable: true - external_web3_ethereum_enabled: - type: boolean - nullable: true - external_zoom_client_id: - type: string - nullable: true - external_zoom_email_optional: - type: boolean - nullable: true - external_zoom_enabled: - type: boolean - nullable: true - external_zoom_secret: - type: string - nullable: true - hook_custom_access_token_enabled: - type: boolean - nullable: true - hook_custom_access_token_uri: - type: string - nullable: true - hook_custom_access_token_secrets: - type: string - nullable: true - hook_mfa_verification_attempt_enabled: - type: boolean - nullable: true - hook_mfa_verification_attempt_uri: - type: string - nullable: true - hook_mfa_verification_attempt_secrets: - type: string - nullable: true - hook_password_verification_attempt_enabled: - type: boolean - nullable: true - hook_password_verification_attempt_uri: - type: string - nullable: true - hook_password_verification_attempt_secrets: - type: string - nullable: true - hook_send_sms_enabled: - type: boolean - nullable: true - hook_send_sms_uri: - type: string - nullable: true - hook_send_sms_secrets: - type: string - nullable: true - hook_send_email_enabled: - type: boolean - nullable: true - hook_send_email_uri: - type: string - nullable: true - hook_send_email_secrets: - type: string - nullable: true - hook_before_user_created_enabled: - type: boolean - nullable: true - hook_before_user_created_uri: - type: string - nullable: true - hook_before_user_created_secrets: - type: string - nullable: true - hook_after_user_created_enabled: - type: boolean - nullable: true - hook_after_user_created_uri: - type: string - nullable: true - hook_after_user_created_secrets: - type: string - nullable: true - jwt_exp: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - mailer_allow_unverified_email_sign_ins: - type: boolean - nullable: true - mailer_autoconfirm: - type: boolean - nullable: true - mailer_otp_exp: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - mailer_otp_length: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - mailer_secure_email_change_enabled: - type: boolean - nullable: true - mailer_subjects_confirmation: - type: string - nullable: true - mailer_subjects_email_change: - type: string - nullable: true - mailer_subjects_invite: - type: string - nullable: true - mailer_subjects_magic_link: - type: string - nullable: true - mailer_subjects_reauthentication: - type: string - nullable: true - mailer_subjects_recovery: - type: string - nullable: true - mailer_subjects_password_changed_notification: - type: string - nullable: true - mailer_subjects_email_changed_notification: - type: string - nullable: true - mailer_subjects_phone_changed_notification: - type: string - nullable: true - mailer_subjects_mfa_factor_enrolled_notification: - type: string - nullable: true - mailer_subjects_mfa_factor_unenrolled_notification: - type: string - nullable: true - mailer_subjects_identity_linked_notification: - type: string - nullable: true - mailer_subjects_identity_unlinked_notification: - type: string - nullable: true - mailer_templates_confirmation_content: - type: string - nullable: true - mailer_templates_email_change_content: - type: string - nullable: true - mailer_templates_invite_content: - type: string - nullable: true - mailer_templates_magic_link_content: - type: string - nullable: true - mailer_templates_reauthentication_content: - type: string - nullable: true - mailer_templates_recovery_content: - type: string - nullable: true - mailer_templates_password_changed_notification_content: - type: string - nullable: true - mailer_templates_email_changed_notification_content: - type: string - nullable: true - mailer_templates_phone_changed_notification_content: - type: string - nullable: true - mailer_templates_mfa_factor_enrolled_notification_content: - type: string - nullable: true - mailer_templates_mfa_factor_unenrolled_notification_content: - type: string - nullable: true - mailer_templates_identity_linked_notification_content: - type: string - nullable: true - mailer_templates_identity_unlinked_notification_content: - type: string - nullable: true - mailer_notifications_password_changed_enabled: - type: boolean - nullable: true - mailer_notifications_email_changed_enabled: - type: boolean - nullable: true - mailer_notifications_phone_changed_enabled: - type: boolean - nullable: true - mailer_notifications_mfa_factor_enrolled_enabled: - type: boolean - nullable: true - mailer_notifications_mfa_factor_unenrolled_enabled: - type: boolean - nullable: true - mailer_notifications_identity_linked_enabled: - type: boolean - nullable: true - mailer_notifications_identity_unlinked_enabled: - type: boolean - nullable: true - mfa_max_enrolled_factors: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - mfa_totp_enroll_enabled: - type: boolean - nullable: true - mfa_totp_verify_enabled: - type: boolean - nullable: true - mfa_phone_enroll_enabled: - type: boolean - nullable: true - mfa_phone_verify_enabled: - type: boolean - nullable: true - mfa_web_authn_enroll_enabled: - type: boolean - nullable: true - mfa_web_authn_verify_enabled: - type: boolean - nullable: true - passkey_enabled: - type: boolean - webauthn_rp_display_name: - type: string - nullable: true - webauthn_rp_id: - type: string - nullable: true - webauthn_rp_origins: - type: string - nullable: true - mfa_phone_otp_length: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - mfa_phone_template: - type: string - nullable: true - mfa_phone_max_frequency: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - nimbus_oauth_client_id: - type: string - nullable: true - nimbus_oauth_email_optional: - type: boolean - nullable: true - nimbus_oauth_client_secret: - type: string - nullable: true - password_hibp_enabled: - type: boolean - nullable: true - password_min_length: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - password_required_characters: - type: string - enum: - - abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:0123456789 - - abcdefghijklmnopqrstuvwxyz:ABCDEFGHIJKLMNOPQRSTUVWXYZ:0123456789 - - abcdefghijklmnopqrstuvwxyz:ABCDEFGHIJKLMNOPQRSTUVWXYZ:0123456789:!@#$%^&*()_+-=[]{};'\\:"|<>?,./`~ - - '' - - null - nullable: true - rate_limit_anonymous_users: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - rate_limit_email_sent: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - rate_limit_sms_sent: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - rate_limit_token_refresh: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - rate_limit_verify: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - rate_limit_otp: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - rate_limit_web3: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - refresh_token_rotation_enabled: - type: boolean - nullable: true - saml_enabled: - type: boolean - nullable: true - saml_external_url: - type: string - nullable: true - saml_allow_encrypted_assertions: - type: boolean - nullable: true - security_sb_forwarded_for_enabled: - type: boolean - nullable: true - security_captcha_enabled: - type: boolean - nullable: true - security_captcha_provider: - type: string - enum: - - turnstile - - hcaptcha - - null - nullable: true - security_captcha_secret: - type: string - nullable: true - security_manual_linking_enabled: - type: boolean - nullable: true - security_refresh_token_reuse_interval: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - security_update_password_require_current_password: - type: boolean - nullable: true - security_update_password_require_reauthentication: - type: boolean - nullable: true - sessions_inactivity_timeout: - type: number - nullable: true - sessions_single_per_user: - type: boolean - nullable: true - sessions_tags: - type: string - nullable: true - sessions_timebox: - type: number - nullable: true - site_url: - type: string - nullable: true - sms_autoconfirm: - type: boolean - nullable: true - sms_max_frequency: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - sms_messagebird_access_key: - type: string - nullable: true - sms_messagebird_originator: - type: string - nullable: true - sms_otp_exp: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - sms_otp_length: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - sms_provider: - type: string - enum: - - messagebird - - textlocal - - twilio - - twilio_verify - - vonage - - null - nullable: true - sms_template: - type: string - nullable: true - sms_test_otp: - type: string - nullable: true - sms_test_otp_valid_until: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - nullable: true - sms_textlocal_api_key: - type: string - nullable: true - sms_textlocal_sender: - type: string - nullable: true - sms_twilio_account_sid: - type: string - nullable: true - sms_twilio_auth_token: - type: string - nullable: true - sms_twilio_content_sid: - type: string - nullable: true - sms_twilio_message_service_sid: - type: string - nullable: true - sms_twilio_verify_account_sid: - type: string - nullable: true - sms_twilio_verify_auth_token: - type: string - nullable: true - sms_twilio_verify_message_service_sid: - type: string - nullable: true - sms_vonage_api_key: - type: string - nullable: true - sms_vonage_api_secret: - type: string - nullable: true - sms_vonage_from: - type: string - nullable: true - smtp_admin_email: - type: string - format: email - pattern: ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$ - nullable: true - smtp_host: - type: string - nullable: true - smtp_max_frequency: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - smtp_pass: - type: string - nullable: true - smtp_port: - type: string - nullable: true - smtp_sender_name: - type: string - nullable: true - smtp_user: - type: string - nullable: true - uri_allow_list: - type: string - nullable: true - oauth_server_enabled: - type: boolean - oauth_server_allow_dynamic_registration: - type: boolean - oauth_server_authorization_path: - type: string - nullable: true - custom_oauth_enabled: - type: boolean - custom_oauth_max_providers: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - required: - - api_max_request_duration - - db_max_pool_size - - db_max_pool_size_unit - - disable_signup - - external_anonymous_users_enabled - - external_apple_additional_client_ids - - external_apple_client_id - - external_apple_email_optional - - external_apple_enabled - - external_apple_secret - - external_azure_client_id - - external_azure_email_optional - - external_azure_enabled - - external_azure_secret - - external_azure_url - - external_bitbucket_client_id - - external_bitbucket_email_optional - - external_bitbucket_enabled - - external_bitbucket_secret - - external_discord_client_id - - external_discord_email_optional - - external_discord_enabled - - external_discord_secret - - external_email_enabled - - external_facebook_client_id - - external_facebook_email_optional - - external_facebook_enabled - - external_facebook_secret - - external_figma_client_id - - external_figma_email_optional - - external_figma_enabled - - external_figma_secret - - external_github_client_id - - external_github_email_optional - - external_github_enabled - - external_github_secret - - external_gitlab_client_id - - external_gitlab_email_optional - - external_gitlab_enabled - - external_gitlab_secret - - external_gitlab_url - - external_google_additional_client_ids - - external_google_client_id - - external_google_email_optional - - external_google_enabled - - external_google_secret - - external_google_skip_nonce_check - - external_kakao_client_id - - external_kakao_email_optional - - external_kakao_enabled - - external_kakao_secret - - external_keycloak_client_id - - external_keycloak_email_optional - - external_keycloak_enabled - - external_keycloak_secret - - external_keycloak_url - - external_linkedin_oidc_client_id - - external_linkedin_oidc_email_optional - - external_linkedin_oidc_enabled - - external_linkedin_oidc_secret - - external_slack_oidc_client_id - - external_slack_oidc_email_optional - - external_slack_oidc_enabled - - external_slack_oidc_secret - - external_notion_client_id - - external_notion_email_optional - - external_notion_enabled - - external_notion_secret - - external_phone_enabled - - external_slack_client_id - - external_slack_email_optional - - external_slack_enabled - - external_slack_secret - - external_spotify_client_id - - external_spotify_email_optional - - external_spotify_enabled - - external_spotify_secret - - external_twitch_client_id - - external_twitch_email_optional - - external_twitch_enabled - - external_twitch_secret - - external_twitter_client_id - - external_twitter_email_optional - - external_twitter_enabled - - external_twitter_secret - - external_x_client_id - - external_x_email_optional - - external_x_enabled - - external_x_secret - - external_workos_client_id - - external_workos_enabled - - external_workos_secret - - external_workos_url - - external_web3_solana_enabled - - external_web3_ethereum_enabled - - external_zoom_client_id - - external_zoom_email_optional - - external_zoom_enabled - - external_zoom_secret - - hook_custom_access_token_enabled - - hook_custom_access_token_uri - - hook_custom_access_token_secrets - - hook_mfa_verification_attempt_enabled - - hook_mfa_verification_attempt_uri - - hook_mfa_verification_attempt_secrets - - hook_password_verification_attempt_enabled - - hook_password_verification_attempt_uri - - hook_password_verification_attempt_secrets - - hook_send_sms_enabled - - hook_send_sms_uri - - hook_send_sms_secrets - - hook_send_email_enabled - - hook_send_email_uri - - hook_send_email_secrets - - hook_before_user_created_enabled - - hook_before_user_created_uri - - hook_before_user_created_secrets - - hook_after_user_created_enabled - - hook_after_user_created_uri - - hook_after_user_created_secrets - - jwt_exp - - mailer_allow_unverified_email_sign_ins - - mailer_autoconfirm - - mailer_otp_exp - - mailer_otp_length - - mailer_secure_email_change_enabled - - mailer_subjects_confirmation - - mailer_subjects_email_change - - mailer_subjects_invite - - mailer_subjects_magic_link - - mailer_subjects_reauthentication - - mailer_subjects_recovery - - mailer_subjects_password_changed_notification - - mailer_subjects_email_changed_notification - - mailer_subjects_phone_changed_notification - - mailer_subjects_mfa_factor_enrolled_notification - - mailer_subjects_mfa_factor_unenrolled_notification - - mailer_subjects_identity_linked_notification - - mailer_subjects_identity_unlinked_notification - - mailer_templates_confirmation_content - - mailer_templates_email_change_content - - mailer_templates_invite_content - - mailer_templates_magic_link_content - - mailer_templates_reauthentication_content - - mailer_templates_recovery_content - - mailer_templates_password_changed_notification_content - - mailer_templates_email_changed_notification_content - - mailer_templates_phone_changed_notification_content - - mailer_templates_mfa_factor_enrolled_notification_content - - mailer_templates_mfa_factor_unenrolled_notification_content - - mailer_templates_identity_linked_notification_content - - mailer_templates_identity_unlinked_notification_content - - mailer_notifications_password_changed_enabled - - mailer_notifications_email_changed_enabled - - mailer_notifications_phone_changed_enabled - - mailer_notifications_mfa_factor_enrolled_enabled - - mailer_notifications_mfa_factor_unenrolled_enabled - - mailer_notifications_identity_linked_enabled - - mailer_notifications_identity_unlinked_enabled - - mfa_max_enrolled_factors - - mfa_totp_enroll_enabled - - mfa_totp_verify_enabled - - mfa_phone_enroll_enabled - - mfa_phone_verify_enabled - - mfa_web_authn_enroll_enabled - - mfa_web_authn_verify_enabled - - passkey_enabled - - webauthn_rp_display_name - - webauthn_rp_id - - webauthn_rp_origins - - mfa_phone_otp_length - - mfa_phone_template - - mfa_phone_max_frequency - - nimbus_oauth_client_id - - nimbus_oauth_email_optional - - nimbus_oauth_client_secret - - password_hibp_enabled - - password_min_length - - password_required_characters - - rate_limit_anonymous_users - - rate_limit_email_sent - - rate_limit_sms_sent - - rate_limit_token_refresh - - rate_limit_verify - - rate_limit_otp - - rate_limit_web3 - - refresh_token_rotation_enabled - - saml_enabled - - saml_external_url - - saml_allow_encrypted_assertions - - security_sb_forwarded_for_enabled - - security_captcha_enabled - - security_captcha_provider - - security_captcha_secret - - security_manual_linking_enabled - - security_refresh_token_reuse_interval - - security_update_password_require_current_password - - security_update_password_require_reauthentication - - sessions_inactivity_timeout - - sessions_single_per_user - - sessions_tags - - sessions_timebox - - site_url - - sms_autoconfirm - - sms_max_frequency - - sms_messagebird_access_key - - sms_messagebird_originator - - sms_otp_exp - - sms_otp_length - - sms_provider - - sms_template - - sms_test_otp - - sms_test_otp_valid_until - - sms_textlocal_api_key - - sms_textlocal_sender - - sms_twilio_account_sid - - sms_twilio_auth_token - - sms_twilio_content_sid - - sms_twilio_message_service_sid - - sms_twilio_verify_account_sid - - sms_twilio_verify_auth_token - - sms_twilio_verify_message_service_sid - - sms_vonage_api_key - - sms_vonage_api_secret - - sms_vonage_from - - smtp_admin_email - - smtp_host - - smtp_max_frequency - - smtp_pass - - smtp_port - - smtp_sender_name - - smtp_user - - uri_allow_list - - oauth_server_enabled - - oauth_server_allow_dynamic_registration - - oauth_server_authorization_path - - custom_oauth_enabled - - custom_oauth_max_providers - UpdateAuthConfigBody: - type: object - properties: - site_url: - type: string - pattern: ^[^,]+$ - nullable: true - disable_signup: - type: boolean - nullable: true - jwt_exp: - type: integer - minimum: 0 - maximum: 604800 - nullable: true - smtp_admin_email: - type: string - format: email - pattern: ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$ - nullable: true - smtp_host: - type: string - nullable: true - smtp_port: - type: string - nullable: true - smtp_user: - type: string - nullable: true - smtp_pass: - type: string - nullable: true - smtp_max_frequency: - type: integer - minimum: 0 - maximum: 32767 - nullable: true - smtp_sender_name: - type: string - nullable: true - mailer_allow_unverified_email_sign_ins: - type: boolean - nullable: true - mailer_autoconfirm: - type: boolean - nullable: true - mailer_subjects_invite: - type: string - nullable: true - mailer_subjects_confirmation: - type: string - nullable: true - mailer_subjects_recovery: - type: string - nullable: true - mailer_subjects_email_change: - type: string - nullable: true - mailer_subjects_magic_link: - type: string - nullable: true - mailer_subjects_reauthentication: - type: string - nullable: true - mailer_subjects_password_changed_notification: - type: string - nullable: true - mailer_subjects_email_changed_notification: - type: string - nullable: true - mailer_subjects_phone_changed_notification: - type: string - nullable: true - mailer_subjects_mfa_factor_enrolled_notification: - type: string - nullable: true - mailer_subjects_mfa_factor_unenrolled_notification: - type: string - nullable: true - mailer_subjects_identity_linked_notification: - type: string - nullable: true - mailer_subjects_identity_unlinked_notification: - type: string - nullable: true - mailer_templates_invite_content: - type: string - nullable: true - mailer_templates_confirmation_content: - type: string - nullable: true - mailer_templates_recovery_content: - type: string - nullable: true - mailer_templates_email_change_content: - type: string - nullable: true - mailer_templates_magic_link_content: - type: string - nullable: true - mailer_templates_reauthentication_content: - type: string - nullable: true - mailer_templates_password_changed_notification_content: - type: string - nullable: true - mailer_templates_email_changed_notification_content: - type: string - nullable: true - mailer_templates_phone_changed_notification_content: - type: string - nullable: true - mailer_templates_mfa_factor_enrolled_notification_content: - type: string - nullable: true - mailer_templates_mfa_factor_unenrolled_notification_content: - type: string - nullable: true - mailer_templates_identity_linked_notification_content: - type: string - nullable: true - mailer_templates_identity_unlinked_notification_content: - type: string - nullable: true - mailer_notifications_password_changed_enabled: - type: boolean - nullable: true - mailer_notifications_email_changed_enabled: - type: boolean - nullable: true - mailer_notifications_phone_changed_enabled: - type: boolean - nullable: true - mailer_notifications_mfa_factor_enrolled_enabled: - type: boolean - nullable: true - mailer_notifications_mfa_factor_unenrolled_enabled: - type: boolean - nullable: true - mailer_notifications_identity_linked_enabled: - type: boolean - nullable: true - mailer_notifications_identity_unlinked_enabled: - type: boolean - nullable: true - mfa_max_enrolled_factors: - type: integer - minimum: 0 - maximum: 2147483647 - nullable: true - uri_allow_list: - type: string - nullable: true - external_anonymous_users_enabled: - type: boolean - nullable: true - external_email_enabled: - type: boolean - nullable: true - external_phone_enabled: - type: boolean - nullable: true - saml_enabled: - type: boolean - nullable: true - saml_external_url: - type: string - pattern: ^[^,]+$ - nullable: true - security_sb_forwarded_for_enabled: - type: boolean - nullable: true - security_captcha_enabled: - type: boolean - nullable: true - security_captcha_provider: - type: string - enum: - - turnstile - - hcaptcha - - null - nullable: true - security_captcha_secret: - type: string - nullable: true - sessions_timebox: - type: number - minimum: 0 - description: Session timebox in hours. Maximum 8760 hours (1 year). - nullable: true - sessions_inactivity_timeout: - type: number - minimum: 0 - description: Session inactivity timeout in hours. Maximum 8760 hours (1 year). - nullable: true - sessions_single_per_user: - type: boolean - nullable: true - sessions_tags: - type: string - pattern: ^\s*([a-zA-Z0-9_-]+(\s*,+\s*)?)*\s*$ - nullable: true - rate_limit_anonymous_users: - type: integer - minimum: 1 - maximum: 2147483647 - nullable: true - rate_limit_email_sent: - type: integer - minimum: 1 - maximum: 2147483647 - nullable: true - rate_limit_sms_sent: - type: integer - minimum: 1 - maximum: 2147483647 - nullable: true - rate_limit_verify: - type: integer - minimum: 1 - maximum: 2147483647 - nullable: true - rate_limit_token_refresh: - type: integer - minimum: 1 - maximum: 2147483647 - nullable: true - rate_limit_otp: - type: integer - minimum: 1 - maximum: 2147483647 - nullable: true - rate_limit_web3: - type: integer - minimum: 1 - maximum: 2147483647 - nullable: true - mailer_secure_email_change_enabled: - type: boolean - nullable: true - refresh_token_rotation_enabled: - type: boolean - nullable: true - password_hibp_enabled: - type: boolean - nullable: true - password_min_length: - type: integer - minimum: 6 - maximum: 32767 - nullable: true - password_required_characters: - type: string - enum: - - abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:0123456789 - - abcdefghijklmnopqrstuvwxyz:ABCDEFGHIJKLMNOPQRSTUVWXYZ:0123456789 - - abcdefghijklmnopqrstuvwxyz:ABCDEFGHIJKLMNOPQRSTUVWXYZ:0123456789:!@#$%^&*()_+-=[]{};'\\:"|<>?,./`~ - - '' - - null - nullable: true - security_manual_linking_enabled: - type: boolean - nullable: true - security_update_password_require_reauthentication: - type: boolean - nullable: true - security_update_password_require_current_password: - type: boolean - description: Require the user's current password when updating their password. - nullable: true - security_refresh_token_reuse_interval: - type: integer - minimum: 0 - maximum: 2147483647 - description: >- - Refresh token reuse interval in seconds. Maximum 300 seconds (5 - minutes). - nullable: true - mailer_otp_exp: - type: integer - minimum: 0 - maximum: 2147483647 - mailer_otp_length: - type: integer - minimum: 6 - maximum: 10 - nullable: true - sms_autoconfirm: - type: boolean - nullable: true - sms_max_frequency: - type: integer - minimum: 0 - maximum: 32767 - nullable: true - sms_otp_exp: - type: integer - minimum: 0 - maximum: 2147483647 - nullable: true - sms_otp_length: - type: integer - minimum: 0 - maximum: 32767 - sms_provider: - type: string - enum: - - messagebird - - textlocal - - twilio - - twilio_verify - - vonage - - null - nullable: true - sms_messagebird_access_key: - type: string - nullable: true - sms_messagebird_originator: - type: string - nullable: true - sms_test_otp: - type: string - pattern: ^([0-9]{1,15}=[0-9]+,?)*$ - nullable: true - sms_test_otp_valid_until: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - nullable: true - sms_textlocal_api_key: - type: string - nullable: true - sms_textlocal_sender: - type: string - nullable: true - sms_twilio_account_sid: - type: string - nullable: true - sms_twilio_auth_token: - type: string - nullable: true - sms_twilio_content_sid: - type: string - nullable: true - sms_twilio_message_service_sid: - type: string - nullable: true - sms_twilio_verify_account_sid: - type: string - nullable: true - sms_twilio_verify_auth_token: - type: string - nullable: true - sms_twilio_verify_message_service_sid: - type: string - nullable: true - sms_vonage_api_key: - type: string - nullable: true - sms_vonage_api_secret: - type: string - nullable: true - sms_vonage_from: - type: string - nullable: true - sms_template: - type: string - nullable: true - hook_mfa_verification_attempt_enabled: - type: boolean - nullable: true - hook_mfa_verification_attempt_uri: - type: string - nullable: true - hook_mfa_verification_attempt_secrets: - type: string - nullable: true - hook_password_verification_attempt_enabled: - type: boolean - nullable: true - hook_password_verification_attempt_uri: - type: string - nullable: true - hook_password_verification_attempt_secrets: - type: string - nullable: true - hook_custom_access_token_enabled: - type: boolean - nullable: true - hook_custom_access_token_uri: - type: string - nullable: true - hook_custom_access_token_secrets: - type: string - nullable: true - hook_send_sms_enabled: - type: boolean - nullable: true - hook_send_sms_uri: - type: string - nullable: true - hook_send_sms_secrets: - type: string - nullable: true - hook_send_email_enabled: - type: boolean - nullable: true - hook_send_email_uri: - type: string - nullable: true - hook_send_email_secrets: - type: string - nullable: true - hook_before_user_created_enabled: - type: boolean - nullable: true - hook_before_user_created_uri: - type: string - nullable: true - hook_before_user_created_secrets: - type: string - nullable: true - hook_after_user_created_enabled: - type: boolean - nullable: true - hook_after_user_created_uri: - type: string - nullable: true - hook_after_user_created_secrets: - type: string - nullable: true - external_apple_enabled: - type: boolean - nullable: true - external_apple_client_id: - type: string - nullable: true - external_apple_email_optional: - type: boolean - nullable: true - external_apple_secret: - type: string - nullable: true - external_apple_additional_client_ids: - type: string - nullable: true - external_azure_enabled: - type: boolean - nullable: true - external_azure_client_id: - type: string - nullable: true - external_azure_email_optional: - type: boolean - nullable: true - external_azure_secret: - type: string - nullable: true - external_azure_url: - type: string - nullable: true - external_bitbucket_enabled: - type: boolean - nullable: true - external_bitbucket_client_id: - type: string - nullable: true - external_bitbucket_email_optional: - type: boolean - nullable: true - external_bitbucket_secret: - type: string - nullable: true - external_discord_enabled: - type: boolean - nullable: true - external_discord_client_id: - type: string - nullable: true - external_discord_email_optional: - type: boolean - nullable: true - external_discord_secret: - type: string - nullable: true - external_facebook_enabled: - type: boolean - nullable: true - external_facebook_client_id: - type: string - nullable: true - external_facebook_email_optional: - type: boolean - nullable: true - external_facebook_secret: - type: string - nullable: true - external_figma_enabled: - type: boolean - nullable: true - external_figma_client_id: - type: string - nullable: true - external_figma_email_optional: - type: boolean - nullable: true - external_figma_secret: - type: string - nullable: true - external_github_enabled: - type: boolean - nullable: true - external_github_client_id: - type: string - nullable: true - external_github_email_optional: - type: boolean - nullable: true - external_github_secret: - type: string - nullable: true - external_gitlab_enabled: - type: boolean - nullable: true - external_gitlab_client_id: - type: string - nullable: true - external_gitlab_email_optional: - type: boolean - nullable: true - external_gitlab_secret: - type: string - nullable: true - external_gitlab_url: - type: string - nullable: true - external_google_enabled: - type: boolean - nullable: true - external_google_client_id: - type: string - nullable: true - external_google_email_optional: - type: boolean - nullable: true - external_google_secret: - type: string - nullable: true - external_google_additional_client_ids: - type: string - nullable: true - external_google_skip_nonce_check: - type: boolean - nullable: true - external_kakao_enabled: - type: boolean - nullable: true - external_kakao_client_id: - type: string - nullable: true - external_kakao_email_optional: - type: boolean - nullable: true - external_kakao_secret: - type: string - nullable: true - external_keycloak_enabled: - type: boolean - nullable: true - external_keycloak_client_id: - type: string - nullable: true - external_keycloak_email_optional: - type: boolean - nullable: true - external_keycloak_secret: - type: string - nullable: true - external_keycloak_url: - type: string - nullable: true - external_linkedin_oidc_enabled: - type: boolean - nullable: true - external_linkedin_oidc_client_id: - type: string - nullable: true - external_linkedin_oidc_email_optional: - type: boolean - nullable: true - external_linkedin_oidc_secret: - type: string - nullable: true - external_slack_oidc_enabled: - type: boolean - nullable: true - external_slack_oidc_client_id: - type: string - nullable: true - external_slack_oidc_email_optional: - type: boolean - nullable: true - external_slack_oidc_secret: - type: string - nullable: true - external_notion_enabled: - type: boolean - nullable: true - external_notion_client_id: - type: string - nullable: true - external_notion_email_optional: - type: boolean - nullable: true - external_notion_secret: - type: string - nullable: true - external_slack_enabled: - type: boolean - nullable: true - external_slack_client_id: - type: string - nullable: true - external_slack_email_optional: - type: boolean - nullable: true - external_slack_secret: - type: string - nullable: true - external_spotify_enabled: - type: boolean - nullable: true - external_spotify_client_id: - type: string - nullable: true - external_spotify_email_optional: - type: boolean - nullable: true - external_spotify_secret: - type: string - nullable: true - external_twitch_enabled: - type: boolean - nullable: true - external_twitch_client_id: - type: string - nullable: true - external_twitch_email_optional: - type: boolean - nullable: true - external_twitch_secret: - type: string - nullable: true - external_twitter_enabled: - type: boolean - nullable: true - external_twitter_client_id: - type: string - nullable: true - external_twitter_email_optional: - type: boolean - nullable: true - external_twitter_secret: - type: string - nullable: true - external_x_enabled: - type: boolean - nullable: true - external_x_client_id: - type: string - nullable: true - external_x_email_optional: - type: boolean - nullable: true - external_x_secret: - type: string - nullable: true - external_workos_enabled: - type: boolean - nullable: true - external_workos_client_id: - type: string - nullable: true - external_workos_secret: - type: string - nullable: true - external_workos_url: - type: string - nullable: true - external_web3_solana_enabled: - type: boolean - nullable: true - external_web3_ethereum_enabled: - type: boolean - nullable: true - external_zoom_enabled: - type: boolean - nullable: true - external_zoom_client_id: - type: string - nullable: true - external_zoom_email_optional: - type: boolean - nullable: true - external_zoom_secret: - type: string - nullable: true - db_max_pool_size: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - db_max_pool_size_unit: - type: string - enum: - - connections - - percent - - null - nullable: true - api_max_request_duration: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - mfa_totp_enroll_enabled: - type: boolean - nullable: true - mfa_totp_verify_enabled: - type: boolean - nullable: true - mfa_web_authn_enroll_enabled: - type: boolean - nullable: true - mfa_web_authn_verify_enabled: - type: boolean - nullable: true - passkey_enabled: - type: boolean - webauthn_rp_display_name: - type: string - nullable: true - webauthn_rp_id: - type: string - nullable: true - webauthn_rp_origins: - type: string - nullable: true - mfa_phone_enroll_enabled: - type: boolean - nullable: true - mfa_phone_verify_enabled: - type: boolean - nullable: true - mfa_phone_max_frequency: - type: integer - minimum: 0 - maximum: 32767 - nullable: true - mfa_phone_otp_length: - type: integer - minimum: 0 - maximum: 32767 - nullable: true - mfa_phone_template: - type: string - nullable: true - nimbus_oauth_client_id: - type: string - nullable: true - nimbus_oauth_client_secret: - type: string - nullable: true - oauth_server_enabled: - type: boolean - nullable: true - oauth_server_allow_dynamic_registration: - type: boolean - nullable: true - oauth_server_authorization_path: - type: string - nullable: true - custom_oauth_enabled: - type: boolean - example: - site_url: https://app.example.com - disable_signup: false - jwt_exp: 3600 - CreateThirdPartyAuthBody: - type: object - properties: - oidc_issuer_url: - type: string - jwks_url: - type: string - custom_jwks: {} - example: - oidc_issuer_url: https://login.acme.com - jwks_url: https://login.acme.com/.well-known/jwks.json - ThirdPartyAuth_Output: - type: object - properties: - id: - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - type: - type: string - oidc_issuer_url: - type: string - nullable: true - jwks_url: - type: string - nullable: true - custom_jwks: - nullable: true - resolved_jwks: - nullable: true - inserted_at: - type: string - updated_at: - type: string - resolved_at: - type: string - nullable: true - required: - - id - - type - - inserted_at - - updated_at - GetProjectAvailableRestoreVersionsResponse_Output: - type: object - properties: - available_versions: - type: array - items: - type: object - properties: - version: - type: string - release_channel: - type: string - enum: - - internal - - alpha - - beta - - ga - - withdrawn - - preview - postgres_engine: - type: string - enum: - - '13' - - '14' - - '15' - - '17' - - 17-oriole - required: - - version - - release_channel - - postgres_engine - required: - - available_versions - ListProjectAddonsResponse_Output: - type: object - properties: - selected_addons: - type: array - items: - type: object - properties: - type: - type: string - enum: - - custom_domain - - compute_instance - - pitr - - ipv4 - - auth_mfa_phone - - auth_mfa_web_authn - - log_drain - - etl_pipeline - variant: - type: object - properties: - id: - anyOf: - - type: string - enum: - - ci_micro - - ci_small - - ci_medium - - ci_large - - ci_xlarge - - ci_2xlarge - - ci_4xlarge - - ci_8xlarge - - ci_12xlarge - - ci_16xlarge - - ci_24xlarge - - ci_24xlarge_optimized_cpu - - ci_24xlarge_optimized_memory - - ci_24xlarge_high_memory - - ci_48xlarge - - ci_48xlarge_optimized_cpu - - ci_48xlarge_optimized_memory - - ci_48xlarge_high_memory - - type: string - enum: - - cd_default - - type: string - enum: - - pitr_7 - - pitr_14 - - pitr_28 - - type: string - enum: - - ipv4_default - - type: string - enum: - - auth_mfa_phone_default - - type: string - enum: - - auth_mfa_web_authn_default - - type: string - enum: - - log_drain_default - - type: string - enum: - - etl_pipeline_default - name: - type: string - price: - type: object - properties: - description: - type: string - type: - type: string - enum: - - fixed - - usage - interval: - type: string - enum: - - monthly - - hourly - amount: - type: number - required: - - description - - type - - interval - - amount - meta: - $ref: '#/components/schemas/JsonValue_Output' - required: - - id - - name - - price - required: - - type - - variant - available_addons: - type: array - items: - type: object - properties: - type: - type: string - enum: - - custom_domain - - compute_instance - - pitr - - ipv4 - - auth_mfa_phone - - auth_mfa_web_authn - - log_drain - - etl_pipeline - name: - type: string - variants: - type: array - items: - type: object - properties: - id: - anyOf: - - type: string - enum: - - ci_micro - - ci_small - - ci_medium - - ci_large - - ci_xlarge - - ci_2xlarge - - ci_4xlarge - - ci_8xlarge - - ci_12xlarge - - ci_16xlarge - - ci_24xlarge - - ci_24xlarge_optimized_cpu - - ci_24xlarge_optimized_memory - - ci_24xlarge_high_memory - - ci_48xlarge - - ci_48xlarge_optimized_cpu - - ci_48xlarge_optimized_memory - - ci_48xlarge_high_memory - - type: string - enum: - - cd_default - - type: string - enum: - - pitr_7 - - pitr_14 - - pitr_28 - - type: string - enum: - - ipv4_default - - type: string - enum: - - auth_mfa_phone_default - - type: string - enum: - - auth_mfa_web_authn_default - - type: string - enum: - - log_drain_default - - type: string - enum: - - etl_pipeline_default - name: - type: string - price: - type: object - properties: - description: - type: string - type: - type: string - enum: - - fixed - - usage - interval: - type: string - enum: - - monthly - - hourly - amount: - type: number - required: - - description - - type - - interval - - amount - meta: - $ref: '#/components/schemas/JsonValue_Output' - required: - - id - - name - - price - required: - - type - - name - - variants - required: - - selected_addons - - available_addons - ApplyProjectAddonBody: - type: object - properties: - addon_variant: - anyOf: - - type: string - enum: - - ci_micro - - ci_small - - ci_medium - - ci_large - - ci_xlarge - - ci_2xlarge - - ci_4xlarge - - ci_8xlarge - - ci_12xlarge - - ci_16xlarge - - ci_24xlarge - - ci_24xlarge_optimized_cpu - - ci_24xlarge_optimized_memory - - ci_24xlarge_high_memory - - ci_48xlarge - - ci_48xlarge_optimized_cpu - - ci_48xlarge_optimized_memory - - ci_48xlarge_high_memory - - type: string - enum: - - cd_default - - type: string - enum: - - pitr_7 - - pitr_14 - - pitr_28 - - type: string - enum: - - ipv4_default - addon_type: - type: string - enum: - - custom_domain - - compute_instance - - pitr - - ipv4 - - auth_mfa_phone - - auth_mfa_web_authn - - log_drain - - etl_pipeline - required: - - addon_variant - - addon_type - example: - addon_variant: pitr_7 - addon_type: pitr - ProjectClaimTokenResponse_Output: - type: object - properties: - token_alias: - type: string - expires_at: - type: string - created_at: - type: string - created_by: - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - required: - - token_alias - - expires_at - - created_at - - created_by - CreateProjectClaimTokenResponse_Output: - type: object - properties: - token: - type: string - token_alias: - type: string - expires_at: - type: string - created_at: - type: string - created_by: - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - required: - - token - - token_alias - - expires_at - - created_at - - created_by - V1ProjectAdvisorsResponse_Output: - type: object - properties: - lints: - type: array - items: - type: object - properties: - name: - type: string - enum: - - unindexed_foreign_keys - - auth_users_exposed - - auth_rls_initplan - - no_primary_key - - unused_index - - multiple_permissive_policies - - policy_exists_rls_disabled - - rls_enabled_no_policy - - duplicate_index - - security_definer_view - - function_search_path_mutable - - rls_disabled_in_public - - extension_in_public - - rls_references_user_metadata - - materialized_view_in_api - - foreign_table_in_api - - unsupported_reg_types - - auth_otp_long_expiry - - auth_otp_short_length - - ssl_not_enforced - - log_connections_not_enabled - - network_restrictions_not_set - - password_requirements_min_length - - pitr_not_enabled - - auth_leaked_password_protection - - auth_insufficient_mfa_options - - auth_password_policy_missing - - leaked_service_key - - no_backup_admin - - vulnerable_postgres_version - - db_not_reachable - - db_connection_failing - - db_connection_limit_reached - - instance_telemetry_lost - - instance_db_down - - instance_alert_firing - - log_data_api_error_rate_high - - log_auth_error_rate_high - - log_storage_error_rate_high - - log_edge_function_error_rate_high - - realtime_authorization_timeouts - - realtime_postgres_changes_spike - - realtime_concurrent_users_near_limit - - project_not_active - - advisor_check_unavailable - title: - type: string - level: - type: string - enum: - - ERROR - - WARN - - INFO - facing: - type: string - enum: - - EXTERNAL - categories: - type: array - items: - type: string - enum: - - PERFORMANCE - - SECURITY - - HEALTH - x-ignore-array-items-must-be-objects: true - description: - type: string - detail: - type: string - remediation: - type: string - metadata: - type: object - properties: - schema: - type: string - name: - type: string - entity: - type: string - type: - type: string - enum: - - table - - view - - materialized view - - foreign table - - auth - - function - - extension - - compliance - - health - fkey_name: - type: string - fkey_columns: - x-ignore-array-items-must-be-objects: true - type: array - items: - type: number - cache_key: - type: string - observed_at: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ - required: - - name - - title - - level - - facing - - categories - - description - - detail - - remediation - - cache_key - required: - - lints - AnalyticsResponse_Output: - type: object - properties: - result: - type: array - items: {} - error: - anyOf: - - type: string - - type: object - properties: - code: - type: number - errors: - type: array - items: - type: object - properties: - domain: - type: string - location: - type: string - locationType: - type: string - message: - type: string - reason: - type: string - required: - - domain - - location - - locationType - - message - - reason - message: - type: string - status: - type: string - required: - - code - - errors - - message - - status - V1GetUsageApiCountResponse_Output: - type: object - properties: - result: - type: array - items: - type: object - properties: - timestamp: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|))$ - total_auth_requests: - type: number - total_realtime_requests: - type: number - total_rest_requests: - type: number - total_storage_requests: - type: number - required: - - timestamp - - total_auth_requests - - total_realtime_requests - - total_rest_requests - - total_storage_requests - error: - anyOf: - - type: string - - type: object - properties: - code: - type: number - errors: - type: array - items: - type: object - properties: - domain: - type: string - location: - type: string - locationType: - type: string - message: - type: string - reason: - type: string - required: - - domain - - location - - locationType - - message - - reason - message: - type: string - status: - type: string - required: - - code - - errors - - message - - status - V1GetUsageApiRequestsCountResponse_Output: - type: object - properties: - result: - type: array - items: - type: object - properties: - count: - type: number - required: - - count - error: - anyOf: - - type: string - - type: object - properties: - code: - type: number - errors: - type: array - items: - type: object - properties: - domain: - type: string - location: - type: string - locationType: - type: string - message: - type: string - reason: - type: string - required: - - domain - - location - - locationType - - message - - reason - message: - type: string - status: - type: string - required: - - code - - errors - - message - - status - CreateRoleBody: - type: object - properties: - read_only: - type: boolean - required: - - read_only - example: - read_only: true - CreateRoleResponse_Output: - type: object - properties: - role: - type: string - minLength: 1 - password: - type: string - minLength: 1 - ttl_seconds: - type: integer - minimum: 1 - maximum: 9007199254740991 - format: int64 - required: - - role - - password - - ttl_seconds - DeleteRolesResponse_Output: - type: object - properties: - message: - type: string - enum: - - ok - required: - - message - V1ListMigrationsResponse_Output: - type: array - items: - type: object - properties: - version: - type: string - name: - type: string - required: - - version - V1CreateMigrationBody: - type: object - properties: - query: - type: string - minLength: 1 - name: - type: string - rollback: - type: string - required: - - query - example: - query: create table public.widgets(id bigint primary key); - name: create_widgets_table - rollback: drop table if exists public.widgets; - V1UpsertMigrationBody: - type: object - properties: - query: - type: string - minLength: 1 - name: - type: string - rollback: - type: string - required: - - query - example: - query: create table public.widgets(id bigint primary key); - name: create_widgets_table - rollback: drop table if exists public.widgets; - V1GetMigrationResponse_Output: - type: object - properties: - version: - type: string - name: - type: string - statements: - type: array - items: - type: string - rollback: - type: array - items: - type: string - created_by: - type: string - idempotency_key: - type: string - required: - - version - V1PatchMigrationBody: - type: object - properties: - name: - type: string - rollback: - type: string - example: - name: create_widgets_table - rollback: drop table if exists public.widgets; - V1RunQueryBody: - type: object - properties: - query: - type: string - minLength: 1 - parameters: - type: array - items: {} - read_only: - type: boolean - required: - - query - example: - query: select * from pg_stat_activity limit 1; - read_only: true - V1ReadOnlyQueryBody: - type: object - properties: - query: - type: string - minLength: 1 - parameters: - type: array - items: {} - required: - - query - example: - query: select * from pg_stat_activity limit 1; - GetProjectDbMetadataResponse_Output: - type: object - properties: - databases: - type: array - items: - type: object - properties: - name: - type: string - schemas: - type: array - items: - type: object - properties: - name: - type: string - required: - - name - required: - - name - - schemas - required: - - databases - V1UpdatePasswordBody: - type: object - properties: - password: - type: string - minLength: 4 - required: - - password - example: - password: correct-horse-battery-staple - V1UpdatePasswordResponse_Output: - type: object - properties: - message: - type: string - required: - - message - JitAccessResponse_Output: - type: object - properties: - user_id: - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - user_roles: - type: array - items: - type: object - properties: - role: - type: string - expires_at: - type: number - allowed_networks: - type: object - properties: - allowed_cidrs: - type: array - items: - type: object - properties: - cidr: - type: string - format: cidrv4 - pattern: ^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$ - required: - - cidr - allowed_cidrs_v6: - type: array - items: - type: object - properties: - cidr: - type: string - format: cidrv6 - pattern: ^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$ - required: - - cidr - branches_only: - type: boolean - required: - - role - required: - - user_roles - AuthorizeJitAccessBody: - type: object - properties: - role: - type: string - minLength: 1 - rhost: - anyOf: - - type: string - format: ipv4 - pattern: ^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$ - - type: string - format: ipv6 - pattern: ^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$ - required: - - role - - rhost - example: - role: postgres - rhost: 203.0.113.10 - JitAuthorizeAccessResponse_Output: - type: object - properties: - user_id: - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - act: - type: string - user_role: - type: object - properties: - role: - type: string - expires_at: - type: number - allowed_networks: - type: object - properties: - allowed_cidrs: - type: array - items: - type: object - properties: - cidr: - type: string - format: cidrv4 - pattern: ^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$ - required: - - cidr - allowed_cidrs_v6: - type: array - items: - type: object - properties: - cidr: - type: string - format: cidrv6 - pattern: ^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$ - required: - - cidr - branches_only: - type: boolean - required: - - role - required: - - user_role - JitListAccessResponse_Output: - type: object - properties: - items: - type: array - items: - anyOf: - - type: object - properties: - user_id: - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - primary_email: - type: string - nullable: true - invite_id: - type: 'null' - expires_at: - type: 'null' - user_roles: - type: array - items: - type: object - properties: - role: - type: string - expires_at: - type: number - allowed_networks: - type: object - properties: - allowed_cidrs: - type: array - items: - type: object - properties: - cidr: - type: string - format: cidrv4 - pattern: ^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$ - required: - - cidr - allowed_cidrs_v6: - type: array - items: - type: object - properties: - cidr: - type: string - format: cidrv6 - pattern: ^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$ - required: - - cidr - branches_only: - type: boolean - required: - - role - required: - - user_id - - primary_email - - invite_id - - expires_at - - user_roles - - type: object - properties: - user_id: - type: 'null' - primary_email: - type: string - invite_id: - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - expires_at: - type: string - user_roles: - type: array - items: - type: object - properties: - role: - type: string - expires_at: - type: number - allowed_networks: - type: object - properties: - allowed_cidrs: - type: array - items: - type: object - properties: - cidr: - type: string - format: cidrv4 - pattern: ^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$ - required: - - cidr - allowed_cidrs_v6: - type: array - items: - type: object - properties: - cidr: - type: string - format: cidrv6 - pattern: ^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$ - required: - - cidr - branches_only: - type: boolean - required: - - role - required: - - user_id - - primary_email - - invite_id - - expires_at - - user_roles - required: - - items - UpdateJitAccessBody: - type: object - properties: - user_id: - type: string - minLength: 1 - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - roles: - type: array - items: - type: object - properties: - role: - type: string - minLength: 1 - expires_at: - type: number - allowed_networks: - type: object - properties: - allowed_cidrs: - type: array - items: - type: object - properties: - cidr: - type: string - format: cidrv4 - pattern: ^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$ - required: - - cidr - allowed_cidrs_v6: - type: array - items: - type: object - properties: - cidr: - type: string - format: cidrv6 - pattern: ^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$ - required: - - cidr - branches_only: - type: boolean - required: - - role - required: - - user_id - - roles - example: - user_id: 55555555-5555-4555-8555-555555555555 - roles: - - role: postgres - expires_at: 1740787200 - allowed_networks: - allowed_cidrs: - - cidr: 203.0.113.0/24 - branches_only: false - InviteExternalUserJitAccessBody: - type: object - properties: - email: - type: string - minLength: 1 - format: email - pattern: ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$ - roles: - type: array - items: - type: object - properties: - role: - type: string - minLength: 1 - expires_at: - type: number - allowed_networks: - type: object - properties: - allowed_cidrs: - type: array - items: - type: object - properties: - cidr: - type: string - format: cidrv4 - pattern: ^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$ - required: - - cidr - allowed_cidrs_v6: - type: array - items: - type: object - properties: - cidr: - type: string - format: cidrv6 - pattern: ^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$ - required: - - cidr - branches_only: - type: boolean - required: - - role - required: - - email - - roles - example: - email: external-user@somedomain.xyz - roles: - - role: postgres - expires_at: 1740787200 - allowed_networks: - allowed_cidrs: - - cidr: 203.0.113.0/24 - branches_only: false - InviteExternalUserJitResponse_Output: - type: object - properties: - email: - type: string - format: email - pattern: ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$ - invite_id: - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - user_roles: - type: array - items: - type: object - properties: - role: - type: string - expires_at: - type: number - allowed_networks: - type: object - properties: - allowed_cidrs: - type: array - items: - type: object - properties: - cidr: - type: string - format: cidrv4 - pattern: ^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$ - required: - - cidr - allowed_cidrs_v6: - type: array - items: - type: object - properties: - cidr: - type: string - format: cidrv6 - pattern: ^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$ - required: - - cidr - branches_only: - type: boolean - required: - - role - required: - - email - - invite_id - - user_roles - AcceptInviteExternalUserJitAccessBody: - type: object - properties: - email: - type: string - minLength: 1 - format: email - pattern: ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$ - token: - type: string - minLength: 1 - required: - - email - - token - example: - email: external-user@somedomain.xyz - token: '' - FunctionResponse_Output: - type: object - properties: - id: - type: string - slug: - type: string - name: - type: string - status: - type: string - enum: - - ACTIVE - - REMOVED - - THROTTLED - version: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - created_at: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - format: int64 - updated_at: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - format: int64 - verify_jwt: - type: boolean - import_map: - type: boolean - entrypoint_path: - type: string - import_map_path: - type: string - ezbr_sha256: - type: string - required: - - id - - slug - - name - - status - - version - - created_at - - updated_at - V1CreateFunctionBody: - type: object - properties: - slug: - type: string - pattern: ^[A-Za-z][A-Za-z0-9_-]*$ - name: - type: string - body: - type: string - verify_jwt: - type: boolean - required: - - slug - - name - - body - example: - slug: hello-world - name: Hello World - body: Deno.serve(() => new Response('Hello, world!')) - verify_jwt: true - BulkUpdateFunctionBody: - type: array - items: - type: object - properties: - id: - type: string - slug: - type: string - pattern: ^[A-Za-z][A-Za-z0-9_-]*$ - name: - type: string - status: - type: string - enum: - - ACTIVE - - REMOVED - - THROTTLED - version: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - created_at: - type: integer - format: int64 - minimum: -9007199254740991 - maximum: 9007199254740991 - verify_jwt: - type: boolean - import_map: - type: boolean - entrypoint_path: - type: string - import_map_path: - type: string - ezbr_sha256: - type: string - required: - - id - - slug - - name - - status - - version - example: - - id: 3c078cce-ad70-4148-9f37-4da362789053 - slug: hello-world - name: Hello World - status: ACTIVE - version: 2 - verify_jwt: true - entrypoint_path: index.ts - BulkUpdateFunctionResponse_Output: - type: object - properties: - functions: - type: array - items: - type: object - properties: - id: - type: string - slug: - type: string - name: - type: string - status: - type: string - enum: - - ACTIVE - - REMOVED - - THROTTLED - version: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - created_at: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - format: int64 - updated_at: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - format: int64 - verify_jwt: - type: boolean - import_map: - type: boolean - entrypoint_path: - type: string - import_map_path: - type: string - ezbr_sha256: - type: string - required: - - id - - slug - - name - - status - - version - - created_at - - updated_at - required: - - functions - FunctionDeployBody: - type: object - properties: - file: - type: array - items: - type: string - format: binary - metadata: - type: object - properties: - entrypoint_path: - type: string - import_map_path: - type: string - static_patterns: - type: array - items: - type: string - verify_jwt: - type: boolean - name: - type: string - required: - - entrypoint_path - required: - - file - - metadata - example: - file: - - ./supabase/functions/hello-world/index.ts - metadata: - entrypoint_path: index.ts - verify_jwt: true - name: Hello World - DeployFunctionResponse_Output: - type: object - properties: - id: - type: string - slug: - type: string - name: - type: string - status: - type: string - enum: - - ACTIVE - - REMOVED - - THROTTLED - version: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - created_at: - type: integer - format: int64 - minimum: -9007199254740991 - maximum: 9007199254740991 - updated_at: - type: integer - format: int64 - minimum: -9007199254740991 - maximum: 9007199254740991 - verify_jwt: - type: boolean - import_map: - type: boolean - entrypoint_path: - type: string - import_map_path: - type: string - ezbr_sha256: - type: string - required: - - id - - slug - - name - - status - - version - FunctionSlugResponse_Output: - type: object - properties: - id: - type: string - slug: - type: string - name: - type: string - status: - type: string - enum: - - ACTIVE - - REMOVED - - THROTTLED - version: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - created_at: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - format: int64 - updated_at: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - format: int64 - verify_jwt: - type: boolean - import_map: - type: boolean - entrypoint_path: - type: string - import_map_path: - type: string - ezbr_sha256: - type: string - required: - - id - - slug - - name - - status - - version - - created_at - - updated_at - StreamableFile: - type: object - properties: {} - V1UpdateFunctionBodyJson: - type: object - properties: - name: - type: string - body: - type: string - verify_jwt: - type: boolean - example: - name: Hello World - body: Deno.serve(() => new Response('Hello again!')) - verify_jwt: true - V1UpdateFunctionBodyEszip: - type: string - format: binary - contentEncoding: binary - description: >+ - Raw Brotli-compressed ESZIP binary file. - - - **Validation requirements** - The file must start with the four magic - bytes `EZBR` (hex: `45 5A 42 52`). - - - The `ezbr_sha256` query parameter must contain the SHA-256 hash of the - uploaded ESZIP payload. - - - **Example** - - ```javascript - - import { readFile } from 'node:fs/promises'; - - import { brotliCompress } from 'node:zlib'; - - import { promisify } from 'node:util'; - - import { createHash } from 'node:crypto'; - - - const compress = promisify(brotliCompress); - - const marker = Buffer.from('EZBR', 'ascii'); - - - const file = await readFile('./function.eszip'); - - const compressed = await compress(file); - - - const payload = Buffer.concat([marker, compressed]); - - - const ezbr_sha256 = createHash('sha256') - .update(payload) - .digest('hex'); - - console.log(ezbr_sha256); - - ``` - - examples: - - Binary Brotli-compressed ESZIP file - V1StorageBucketResponse_Output: - type: object - properties: - id: - type: string - name: - type: string - owner: - type: string - created_at: - type: string - updated_at: - type: string - public: - type: boolean - required: - - id - - name - - owner - - created_at - - updated_at - - public - DiskResponse_Output: - type: object - properties: - attributes: - anyOf: - - type: object - properties: - iops: - type: integer - exclusiveMinimum: true - maximum: 9007199254740991 - minimum: 0 - size_gb: - type: integer - exclusiveMinimum: true - maximum: 9007199254740991 - minimum: 0 - throughput_mibps: - type: integer - exclusiveMinimum: true - maximum: 9007199254740991 - minimum: 0 - type: - type: string - enum: - - gp3 - required: - - iops - - size_gb - - type - - type: object - properties: - iops: - type: integer - exclusiveMinimum: true - maximum: 9007199254740991 - minimum: 0 - size_gb: - type: integer - exclusiveMinimum: true - maximum: 9007199254740991 - minimum: 0 - type: - type: string - enum: - - io2 - required: - - iops - - size_gb - - type - last_modified_at: - type: string - required: - - attributes - DiskRequestBody: - type: object - properties: - attributes: - oneOf: - - type: object - properties: - iops: - type: integer - exclusiveMinimum: true - maximum: 9007199254740991 - minimum: 0 - size_gb: - type: integer - exclusiveMinimum: true - maximum: 9007199254740991 - minimum: 0 - throughput_mibps: - type: integer - exclusiveMinimum: true - maximum: 9007199254740991 - minimum: 0 - type: - type: string - enum: - - gp3 - required: - - iops - - size_gb - - type - - type: object - properties: - iops: - type: integer - exclusiveMinimum: true - maximum: 9007199254740991 - minimum: 0 - size_gb: - type: integer - exclusiveMinimum: true - maximum: 9007199254740991 - minimum: 0 - type: - type: string - enum: - - io2 - required: - - iops - - size_gb - - type - required: - - attributes - example: - attributes: - type: gp3 - size_gb: 100 - iops: 3000 - throughput_mibps: 125 - DiskUtilMetricsResponse_Output: - type: object - properties: - timestamp: - type: string - metrics: - type: object - properties: - fs_size_bytes: - type: number - fs_avail_bytes: - type: number - fs_used_bytes: - type: number - required: - - fs_size_bytes - - fs_avail_bytes - - fs_used_bytes - required: - - timestamp - - metrics - DiskAutoscaleConfig_Output: - type: object - properties: - growth_percent: - type: integer - exclusiveMinimum: 0 - maximum: 9007199254740991 - description: Growth percentage for disk autoscaling - nullable: true - min_increment_gb: - type: integer - exclusiveMinimum: 0 - maximum: 9007199254740991 - description: Minimum increment size for disk autoscaling in GB - nullable: true - max_size_gb: - type: integer - exclusiveMinimum: 0 - maximum: 9007199254740991 - description: Maximum limit the disk size will grow to in GB - nullable: true - required: - - growth_percent - - min_increment_gb - - max_size_gb - StorageConfigResponse_Output: - type: object - properties: - fileSizeLimit: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - format: int64 - features: - type: object - properties: - imageTransformation: - type: object - properties: - enabled: - type: boolean - required: - - enabled - s3Protocol: - type: object - properties: - enabled: - type: boolean - required: - - enabled - purgeCache: - type: object - properties: - enabled: - type: boolean - required: - - enabled - icebergCatalog: - type: object - properties: - enabled: - type: boolean - maxNamespaces: - type: integer - minimum: 0 - maximum: 9007199254740991 - maxTables: - type: integer - minimum: 0 - maximum: 9007199254740991 - maxCatalogs: - type: integer - minimum: 0 - maximum: 9007199254740991 - required: - - enabled - - maxNamespaces - - maxTables - - maxCatalogs - vectorBuckets: - type: object - properties: - enabled: - type: boolean - maxBuckets: - type: integer - minimum: 0 - maximum: 9007199254740991 - maxIndexes: - type: integer - minimum: 0 - maximum: 9007199254740991 - required: - - enabled - - maxBuckets - - maxIndexes - required: - - imageTransformation - - s3Protocol - - purgeCache - - icebergCatalog - - vectorBuckets - capabilities: - type: object - properties: - list_v2: - type: boolean - iceberg_catalog: - type: boolean - object_versioning: - type: boolean - required: - - list_v2 - - iceberg_catalog - - object_versioning - external: - type: object - properties: - upstreamTarget: - type: string - enum: - - main - - canary - required: - - upstreamTarget - migrationVersion: - type: string - nullable: true - required: - - fileSizeLimit - - features - - capabilities - - external - - migrationVersion - UpdateStorageConfigBody: - type: object - properties: - fileSizeLimit: - type: integer - format: int64 - minimum: 0 - maximum: 536870912000 - features: - type: object - properties: - imageTransformation: - type: object - properties: - enabled: - type: boolean - required: - - enabled - s3Protocol: - type: object - properties: - enabled: - type: boolean - required: - - enabled - purgeCache: - type: object - properties: - enabled: - type: boolean - required: - - enabled - icebergCatalog: - type: object - properties: - enabled: - type: boolean - maxNamespaces: - type: integer - minimum: 0 - maximum: 9007199254740991 - maxTables: - type: integer - minimum: 0 - maximum: 9007199254740991 - maxCatalogs: - type: integer - minimum: 0 - maximum: 9007199254740991 - required: - - enabled - - maxNamespaces - - maxTables - - maxCatalogs - vectorBuckets: - type: object - properties: - enabled: - type: boolean - maxBuckets: - type: integer - minimum: 0 - maximum: 9007199254740991 - maxIndexes: - type: integer - minimum: 0 - maximum: 9007199254740991 - required: - - enabled - - maxBuckets - - maxIndexes - external: - type: object - properties: - upstreamTarget: - type: string - enum: - - main - - canary - required: - - upstreamTarget - example: - fileSizeLimit: 10485760 - features: - imageTransformation: - enabled: true - additionalProperties: false - V1PgbouncerConfigResponse_Output: - type: object - properties: - default_pool_size: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - ignore_startup_parameters: - type: string - max_client_conn: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - pool_mode: - type: string - enum: - - transaction - - session - - statement - connection_string: - type: string - server_idle_timeout: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - server_lifetime: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - query_wait_timeout: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - reserve_pool_size: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - SupavisorConfigResponse_Output: - type: object - properties: - identifier: - type: string - database_type: - type: string - enum: - - PRIMARY - - READ_REPLICA - is_using_scram_auth: - type: boolean - db_user: - type: string - db_host: - type: string - db_port: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - db_name: - type: string - connection_string: - type: string - connectionString: - type: string - description: Use connection_string instead - default_pool_size: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - max_client_conn: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - pool_mode: - type: string - enum: - - transaction - - session - required: - - identifier - - database_type - - is_using_scram_auth - - db_user - - db_host - - db_port - - db_name - - connection_string - - connectionString - - default_pool_size - - max_client_conn - - pool_mode - UpdateSupavisorConfigBody: - type: object - properties: - default_pool_size: - type: integer - minimum: 0 - maximum: 3000 - nullable: true - pool_mode: - description: Dedicated pooler mode for the project - type: string - enum: - - transaction - - session - example: - default_pool_size: 25 - pool_mode: transaction - UpdateSupavisorConfigResponse_Output: - type: object - properties: - default_pool_size: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - nullable: true - pool_mode: - type: string - required: - - default_pool_size - - pool_mode - PostgresConfigResponse_Output: - type: object - properties: - effective_cache_size: - type: string - logical_decoding_work_mem: - type: string - pg_stat_statements.max: - type: integer - minimum: 100 - maximum: 45000 - pg_net.batch_size: - type: integer - minimum: 0 - maximum: 32767 - autovacuum_max_workers: - type: integer - minimum: 1 - maximum: 262143 - autovacuum_work_mem: - type: string - cron.log_statement: - type: boolean - log_autovacuum_min_duration: - type: string - description: 'Default unit: ms' - pattern: ^(-?[0-9]+(?:\.[0-9]+)?)(us|ms|s|min|h|d)?$ - log_checkpoints: - type: boolean - log_connections: - type: boolean - log_disconnections: - type: boolean - log_duration: - type: boolean - log_lock_waits: - type: boolean - log_recovery_conflict_waits: - type: boolean - log_replication_commands: - type: boolean - log_startup_progress_interval: - type: string - description: 'Default unit: ms' - pattern: ^(-?[0-9]+(?:\.[0-9]+)?)(us|ms|s|min|h|d)?$ - log_temp_files: - type: string - maintenance_work_mem: - type: string - track_activity_query_size: - type: string - max_connections: - type: integer - minimum: 1 - maximum: 262143 - max_locks_per_transaction: - type: integer - minimum: 10 - maximum: 2147483640 - max_logical_replication_workers: - type: integer - minimum: 0 - maximum: 262143 - max_parallel_maintenance_workers: - type: integer - minimum: 0 - maximum: 1024 - max_parallel_workers: - type: integer - minimum: 0 - maximum: 1024 - max_parallel_workers_per_gather: - type: integer - minimum: 0 - maximum: 1024 - max_replication_slots: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - max_slot_wal_keep_size: - type: string - max_standby_archive_delay: - type: string - max_standby_streaming_delay: - type: string - max_sync_workers_per_subscription: - type: integer - minimum: 0 - maximum: 262143 - max_wal_size: - type: string - max_wal_senders: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - max_worker_processes: - type: integer - minimum: 0 - maximum: 262143 - session_replication_role: - type: string - enum: - - origin - - replica - - local - shared_buffers: - type: string - statement_timeout: - type: string - description: 'Default unit: ms' - pattern: ^(-?[0-9]+(?:\.[0-9]+)?)(us|ms|s|min|h|d)?$ - track_commit_timestamp: - type: boolean - wal_keep_size: - type: string - wal_sender_timeout: - type: string - description: 'Default unit: ms' - pattern: ^(-?[0-9]+(?:\.[0-9]+)?)(us|ms|s|min|h|d)?$ - work_mem: - type: string - checkpoint_timeout: - type: string - description: 'Default unit: s' - pattern: ^(-?[0-9]+(?:\.[0-9]+)?)(us|ms|s|min|h|d)?$ - hot_standby_feedback: - type: boolean - UpdatePostgresConfigBody: - type: object - properties: - effective_cache_size: - type: string - logical_decoding_work_mem: - type: string - pg_stat_statements.max: - type: integer - minimum: 100 - maximum: 45000 - pg_net.batch_size: - type: integer - minimum: 0 - maximum: 32767 - autovacuum_max_workers: - type: integer - minimum: 1 - maximum: 262143 - autovacuum_work_mem: - type: string - cron.log_statement: - type: boolean - log_autovacuum_min_duration: - type: string - description: 'Default unit: ms' - pattern: ^(-?[0-9]+(?:\.[0-9]+)?)(us|ms|s|min|h|d)?$ - log_checkpoints: - type: boolean - log_connections: - type: boolean - log_disconnections: - type: boolean - log_duration: - type: boolean - log_lock_waits: - type: boolean - log_recovery_conflict_waits: - type: boolean - log_replication_commands: - type: boolean - log_startup_progress_interval: - type: string - description: 'Default unit: ms' - pattern: ^(-?[0-9]+(?:\.[0-9]+)?)(us|ms|s|min|h|d)?$ - log_temp_files: - type: string - maintenance_work_mem: - type: string - track_activity_query_size: - type: string - max_connections: - type: integer - minimum: 1 - maximum: 262143 - max_locks_per_transaction: - type: integer - minimum: 10 - maximum: 2147483640 - max_logical_replication_workers: - type: integer - minimum: 0 - maximum: 262143 - max_parallel_maintenance_workers: - type: integer - minimum: 0 - maximum: 1024 - max_parallel_workers: - type: integer - minimum: 0 - maximum: 1024 - max_parallel_workers_per_gather: - type: integer - minimum: 0 - maximum: 1024 - max_replication_slots: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - max_slot_wal_keep_size: - type: string - max_standby_archive_delay: - type: string - max_standby_streaming_delay: - type: string - max_sync_workers_per_subscription: - type: integer - minimum: 0 - maximum: 262143 - max_wal_size: - type: string - max_wal_senders: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - max_worker_processes: - type: integer - minimum: 0 - maximum: 262143 - session_replication_role: - type: string - enum: - - origin - - replica - - local - shared_buffers: - type: string - statement_timeout: - type: string - description: 'Default unit: ms' - pattern: ^(-?[0-9]+(?:\.[0-9]+)?)(us|ms|s|min|h|d)?$ - track_commit_timestamp: - type: boolean - wal_keep_size: - type: string - wal_sender_timeout: - type: string - description: 'Default unit: ms' - pattern: ^(-?[0-9]+(?:\.[0-9]+)?)(us|ms|s|min|h|d)?$ - work_mem: - type: string - checkpoint_timeout: - type: string - description: 'Default unit: s' - pattern: ^(-?[0-9]+(?:\.[0-9]+)?)(us|ms|s|min|h|d)?$ - hot_standby_feedback: - type: boolean - restart_database: - type: boolean - example: - max_connections: 120 - shared_buffers: 256MB - work_mem: 4MB - statement_timeout: 60000ms - additionalProperties: false - RealtimeConfigResponse_Output: - type: object - properties: - private_only: - type: boolean - description: Whether to only allow private channels - nullable: true - connection_pool: - type: integer - minimum: 1 - maximum: 100 - description: Sets connection pool size for Realtime Authorization - nullable: true - postgres_changes_pool: - type: integer - minimum: 1 - maximum: 100 - description: >- - Sets connection pool size used to create Postgres Changes - subscriptions - nullable: true - max_concurrent_users: - type: integer - minimum: 1 - maximum: 300000 - description: Sets maximum number of concurrent users rate limit - nullable: true - max_events_per_second: - type: integer - minimum: 1 - maximum: 50000 - description: Sets maximum number of events per second rate per channel limit - nullable: true - max_bytes_per_second: - type: integer - minimum: 1 - maximum: 10000000 - description: Sets maximum number of bytes per second rate per channel limit - nullable: true - max_channels_per_client: - type: integer - minimum: 1 - maximum: 10000 - description: Sets maximum number of channels per client rate limit - nullable: true - max_joins_per_second: - type: integer - minimum: 1 - maximum: 5000 - description: Sets maximum number of joins per second rate limit - nullable: true - max_presence_events_per_second: - type: integer - minimum: 1 - maximum: 5000 - description: Sets maximum number of presence events per second rate limit - nullable: true - max_payload_size_in_kb: - type: integer - minimum: 1 - maximum: 10000 - description: Sets maximum number of payload size in KB rate limit - nullable: true - suspend: - type: boolean - description: >- - Disables the Realtime service for this project when true. Set to - false to re-enable it. - nullable: true - presence_enabled: - type: boolean - description: Whether to enable presence - admin_suspended_at: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - description: If set, the Realtime service has been suspended by an admin. - nullable: true - required: - - private_only - - connection_pool - - postgres_changes_pool - - max_concurrent_users - - max_events_per_second - - max_bytes_per_second - - max_channels_per_client - - max_joins_per_second - - max_presence_events_per_second - - max_payload_size_in_kb - - suspend - - presence_enabled - - admin_suspended_at - UpdateRealtimeConfigBody: - type: object - properties: - private_only: - type: boolean - description: Whether to only allow private channels - connection_pool: - type: integer - minimum: 1 - maximum: 100 - description: Sets connection pool size for Realtime Authorization - postgres_changes_pool: - type: integer - minimum: 1 - maximum: 100 - description: >- - Sets connection pool size used to create Postgres Changes - subscriptions - max_concurrent_users: - type: integer - minimum: 1 - maximum: 300000 - description: Sets maximum number of concurrent users rate limit - max_events_per_second: - type: integer - minimum: 1 - maximum: 50000 - description: Sets maximum number of events per second rate per channel limit - max_bytes_per_second: - type: integer - minimum: 1 - maximum: 10000000 - description: Sets maximum number of bytes per second rate per channel limit - max_channels_per_client: - type: integer - minimum: 1 - maximum: 10000 - description: Sets maximum number of channels per client rate limit - max_joins_per_second: - type: integer - minimum: 1 - maximum: 5000 - description: Sets maximum number of joins per second rate limit - max_presence_events_per_second: - type: integer - minimum: 1 - maximum: 5000 - description: Sets maximum number of presence events per second rate limit - max_payload_size_in_kb: - type: integer - minimum: 1 - maximum: 10000 - description: Sets maximum number of payload size in KB rate limit - suspend: - type: boolean - description: >- - Disables the Realtime service for this project when true. Set to - false to re-enable it. - presence_enabled: - type: boolean - description: Whether to enable presence - example: - private_only: false - max_concurrent_users: 1000 - max_channels_per_client: 100 - additionalProperties: false - CreateProviderBody: - type: object - properties: - type: - type: string - enum: - - saml - description: What type of provider will be created - metadata_xml: - type: string - metadata_url: - type: string - domains: - type: array - items: - type: string - attribute_mapping: - type: object - properties: - keys: - type: object - additionalProperties: - type: object - properties: - name: - type: string - names: - type: array - items: - type: string - array: - type: boolean - default: - anyOf: - - type: object - properties: {} - - type: number - - type: string - - type: boolean - required: - - keys - name_id_format: - type: string - enum: - - urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified - - urn:oasis:names:tc:SAML:2.0:nameid-format:transient - - urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress - - urn:oasis:names:tc:SAML:2.0:nameid-format:persistent - required: - - type - example: - type: saml - metadata_url: https://sso.acme.com/metadata.xml - domains: - - acme.com - attribute_mapping: - keys: - email: - name: email - first_name: - name: first_name - last_name: - name: last_name - CreateProviderResponse_Output: - type: object - properties: - id: - type: string - saml: - type: object - properties: - entity_id: - type: string - metadata_url: - type: string - metadata_xml: - type: string - attribute_mapping: - type: object - properties: - keys: - type: object - additionalProperties: - type: object - properties: - name: - type: string - names: - type: array - items: - type: string - array: - type: boolean - default: - anyOf: - - type: object - properties: {} - - type: number - - type: string - - type: boolean - required: - - keys - name_id_format: - type: string - enum: - - urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified - - urn:oasis:names:tc:SAML:2.0:nameid-format:transient - - urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress - - urn:oasis:names:tc:SAML:2.0:nameid-format:persistent - required: - - entity_id - domains: - type: array - items: - type: object - properties: - domain: - type: string - created_at: - type: string - updated_at: - type: string - created_at: - type: string - updated_at: - type: string - required: - - id - ListProvidersResponse_Output: - type: object - properties: - items: - type: array - items: - type: object - properties: - id: - type: string - saml: - type: object - properties: - entity_id: - type: string - metadata_url: - type: string - metadata_xml: - type: string - attribute_mapping: - type: object - properties: - keys: - type: object - additionalProperties: - type: object - properties: - name: - type: string - names: - type: array - items: - type: string - array: - type: boolean - default: - anyOf: - - type: object - properties: {} - - type: number - - type: string - - type: boolean - required: - - keys - name_id_format: - type: string - enum: - - urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified - - urn:oasis:names:tc:SAML:2.0:nameid-format:transient - - urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress - - urn:oasis:names:tc:SAML:2.0:nameid-format:persistent - required: - - entity_id - domains: - type: array - items: - type: object - properties: - domain: - type: string - created_at: - type: string - updated_at: - type: string - created_at: - type: string - updated_at: - type: string - required: - - id - required: - - items - GetProviderResponse_Output: - type: object - properties: - id: - type: string - saml: - type: object - properties: - entity_id: - type: string - metadata_url: - type: string - metadata_xml: - type: string - attribute_mapping: - type: object - properties: - keys: - type: object - additionalProperties: - type: object - properties: - name: - type: string - names: - type: array - items: - type: string - array: - type: boolean - default: - anyOf: - - type: object - properties: {} - - type: number - - type: string - - type: boolean - required: - - keys - name_id_format: - type: string - enum: - - urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified - - urn:oasis:names:tc:SAML:2.0:nameid-format:transient - - urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress - - urn:oasis:names:tc:SAML:2.0:nameid-format:persistent - required: - - entity_id - domains: - type: array - items: - type: object - properties: - domain: - type: string - created_at: - type: string - updated_at: - type: string - created_at: - type: string - updated_at: - type: string - required: - - id - UpdateProviderBody: - type: object - properties: - metadata_xml: - type: string - metadata_url: - type: string - domains: - type: array - items: - type: string - attribute_mapping: - type: object - properties: - keys: - type: object - additionalProperties: - type: object - properties: - name: - type: string - names: - type: array - items: - type: string - array: - type: boolean - default: - anyOf: - - type: object - properties: {} - - type: number - - type: string - - type: boolean - required: - - keys - name_id_format: - type: string - enum: - - urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified - - urn:oasis:names:tc:SAML:2.0:nameid-format:transient - - urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress - - urn:oasis:names:tc:SAML:2.0:nameid-format:persistent - example: - metadata_url: https://sso.acme.com/metadata.xml - domains: - - acme.com - - contractors.acme.com - UpdateProviderResponse_Output: - type: object - properties: - id: - type: string - saml: - type: object - properties: - entity_id: - type: string - metadata_url: - type: string - metadata_xml: - type: string - attribute_mapping: - type: object - properties: - keys: - type: object - additionalProperties: - type: object - properties: - name: - type: string - names: - type: array - items: - type: string - array: - type: boolean - default: - anyOf: - - type: object - properties: {} - - type: number - - type: string - - type: boolean - required: - - keys - name_id_format: - type: string - enum: - - urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified - - urn:oasis:names:tc:SAML:2.0:nameid-format:transient - - urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress - - urn:oasis:names:tc:SAML:2.0:nameid-format:persistent - required: - - entity_id - domains: - type: array - items: - type: object - properties: - domain: - type: string - created_at: - type: string - updated_at: - type: string - created_at: - type: string - updated_at: - type: string - required: - - id - DeleteProviderResponse_Output: - type: object - properties: - id: - type: string - saml: - type: object - properties: - entity_id: - type: string - metadata_url: - type: string - metadata_xml: - type: string - attribute_mapping: - type: object - properties: - keys: - type: object - additionalProperties: - type: object - properties: - name: - type: string - names: - type: array - items: - type: string - array: - type: boolean - default: - anyOf: - - type: object - properties: {} - - type: number - - type: string - - type: boolean - required: - - keys - name_id_format: - type: string - enum: - - urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified - - urn:oasis:names:tc:SAML:2.0:nameid-format:transient - - urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress - - urn:oasis:names:tc:SAML:2.0:nameid-format:persistent - required: - - entity_id - domains: - type: array - items: - type: object - properties: - domain: - type: string - created_at: - type: string - updated_at: - type: string - created_at: - type: string - updated_at: - type: string - required: - - id - V1BackupsResponse_Output: - type: object - properties: - region: - type: string - walg_enabled: - type: boolean - pitr_enabled: - type: boolean - backups: - type: array - items: - type: object - properties: - id: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - is_physical_backup: - type: boolean - status: - type: string - enum: - - COMPLETED - - FAILED - - PENDING - - REMOVED - - ARCHIVED - - CANCELLED - inserted_at: - type: string - required: - - id - - is_physical_backup - - status - - inserted_at - physical_backup_data: - type: object - properties: - earliest_physical_backup_date_unix: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - latest_physical_backup_date_unix: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - required: - - region - - walg_enabled - - pitr_enabled - - backups - - physical_backup_data - V1RestorePitrBody: - type: object - properties: - recovery_time_target_unix: - type: integer - minimum: 0 - maximum: 9007199254740991 - format: int64 - required: - - recovery_time_target_unix - example: - recovery_time_target_unix: 1740787200 - V1RestorePointPostBody: - type: object - properties: - name: - type: string - maxLength: 20 - required: - - name - example: - name: before-upgrade - V1RestorePointResponse: - type: object - properties: - name: - type: string - status: - type: string - enum: - - AVAILABLE - - PENDING - - REMOVED - - FAILED - completed_on: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - nullable: true - required: - - name - - status - - completed_on - V1RestoreBackupBody: - type: object - properties: - id: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - required: - - id - example: - id: 12345 - V1BackupScheduleResponse_Output: - type: object - properties: - schedule_for: - type: string - pattern: ^(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?$ - description: 'Time of day to schedule daily backups, in UTC. Format: HH:MM:SS.' - example: '04:00:00' - updated_at: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - description: Timestamp of when the backup schedule was last updated. - example: '2026-05-04T14:40:44+00:00' - required: - - schedule_for - - updated_at - V1UpdateBackupScheduleBody: - type: object - properties: - schedule_for: - type: string - pattern: ^(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?$ - description: 'Time of day to schedule daily backups, in UTC. Format: HH:MM:SS.' - example: '04:00:00' - required: - - schedule_for - example: - schedule_for: '04:00:00' - V1UndoBody: - type: object - properties: - name: - type: string - maxLength: 20 - required: - - name - example: - name: before-upgrade - V1ListEntitlementsResponse_Output: - type: object - properties: - entitlements: - type: array - items: - type: object - properties: - feature: - type: object - properties: - key: - type: string - enum: - - instances.compute_update_available_sizes - - instances.read_replicas - - instances.disk_modifications - - instances.high_availability - - instances.orioledb - - replication.etl - - storage.max_file_size - - storage.max_file_size.configurable - - storage.image_transformations - - storage.vector_buckets - - storage.iceberg_catalog - - storage.purge_cache - - security.audit_logs_days - - security.questionnaire - - security.soc2_report - - security.iso27001_certificate - - security.private_link - - security.enforce_mfa - - log.retention_days - - custom_domain - - vanity_subdomain - - ipv4 - - pitr.available_variants - - log_drains - - audit_log_drains - - branching_limit - - branching_persistent - - auth.mfa_phone - - auth.mfa_web_authn - - auth.mfa_enhanced_security - - auth.hooks - - auth.platform.sso - - auth.custom_jwt_template - - auth.saml_2 - - auth.user_sessions - - auth.leaked_password_protection - - auth.advanced_auth_settings - - auth.performance_settings - - auth.password_hibp - - auth.custom_oauth.max_providers - - backup.retention_days - - backup.restore_to_new_project - - backup.schedule - - function.max_count - - function.size_limit_mb - - realtime.max_concurrent_users - - realtime.max_events_per_second - - realtime.max_joins_per_second - - realtime.max_channels_per_client - - realtime.max_bytes_per_second - - realtime.max_presence_events_per_second - - realtime.max_payload_size_in_kb - - project_scoped_roles - - security.member_roles - - project_pausing - - project_cloning - - project_restore_after_expiry - - assistant.advance_model - - integrations.github_connections - - integrations.github_push_webhooks_limit - - dedicated_pooler - - observability.dashboard_advanced_metrics - - api.members.invitations - - api.members.roles - type: - type: string - enum: - - boolean - - numeric - - set - required: - - key - - type - hasAccess: - type: boolean - type: - type: string - enum: - - boolean - - numeric - - set - config: - anyOf: - - type: object - properties: - enabled: - type: boolean - required: - - enabled - - type: object - properties: - enabled: - type: boolean - value: - type: number - unlimited: - type: boolean - unit: - type: string - required: - - enabled - - value - - unlimited - - unit - - type: object - properties: - enabled: - type: boolean - set: - type: array - items: - type: string - required: - - enabled - - set - required: - - feature - - hasAccess - - type - - config - required: - - entitlements - V1OrganizationMemberResponse_Output: - type: object - properties: - user_id: - type: string - user_name: - type: string - email: - type: string - role_name: - type: string - mfa_enabled: - type: boolean - avatar_url: - type: string - nullable: true - required: - - user_id - - user_name - - mfa_enabled - - avatar_url - V1OrganizationSlugResponse_Output: - type: object - properties: - id: - type: string - name: - type: string - plan: - type: string - enum: - - free - - pro - - team - - enterprise - - platform - opt_in_tags: - type: array - items: - enum: - - AI_SQL_GENERATOR_OPT_IN - - AI_DATA_GENERATOR_OPT_IN - - AI_LOG_GENERATOR_OPT_IN - allowed_release_channels: - type: array - items: - type: string - enum: - - internal - - alpha - - beta - - ga - - withdrawn - - preview - required: - - id - - name - - opt_in_tags - - allowed_release_channels - OrganizationProjectClaimResponse_Output: - type: object - properties: - project: - type: object - properties: - ref: - type: string - name: - type: string - required: - - ref - - name - preview: - type: object - properties: - valid: - type: boolean - warnings: - type: array - items: - type: object - properties: - key: - type: string - message: - type: string - required: - - key - - message - errors: - type: array - items: - type: object - properties: - key: - type: string - message: - type: string - required: - - key - - message - info: - type: array - items: - type: object - properties: - key: - type: string - message: - type: string - required: - - key - - message - members_exceeding_free_project_limit: - type: array - items: - type: object - properties: - name: - type: string - limit: - type: number - required: - - name - - limit - source_subscription_plan: - type: string - enum: - - free - - pro - - team - - enterprise - - platform - target_subscription_plan: - type: string - enum: - - free - - pro - - team - - enterprise - - platform - - null - nullable: true - required: - - valid - - warnings - - errors - - info - - members_exceeding_free_project_limit - - source_subscription_plan - - target_subscription_plan - expires_at: - type: string - created_at: - type: string - created_by: - type: string - format: uuid - pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$ - required: - - project - - preview - - expires_at - - created_at - - created_by - OrganizationProjectsResponse_Output: - type: object - properties: - projects: - type: array - items: - type: object - properties: - ref: - type: string - name: - type: string - cloud_provider: - type: string - region: - type: string - is_branch: - type: boolean - status: - type: string - enum: - - INACTIVE - - ACTIVE_HEALTHY - - ACTIVE_UNHEALTHY - - COMING_UP - - UNKNOWN - - GOING_DOWN - - INIT_FAILED - - REMOVED - - RESTORING - - UPGRADING - - PAUSING - - RESTORE_FAILED - - RESTARTING - - PAUSE_FAILED - - RESIZING - inserted_at: - type: string - databases: - type: array - items: - type: object - properties: - infra_compute_size: - type: string - enum: - - pico - - nano - - micro - - small - - medium - - large - - xlarge - - 2xlarge - - 4xlarge - - 8xlarge - - 12xlarge - - 16xlarge - - 24xlarge - - 24xlarge_optimized_memory - - 24xlarge_optimized_cpu - - 24xlarge_high_memory - - 48xlarge - - 48xlarge_optimized_memory - - 48xlarge_optimized_cpu - - 48xlarge_high_memory - region: - type: string - status: - type: string - enum: - - ACTIVE_HEALTHY - - ACTIVE_UNHEALTHY - - COMING_UP - - GOING_DOWN - - INIT_FAILED - - REMOVED - - RESTORING - - UNKNOWN - - INIT_READ_REPLICA - - INIT_READ_REPLICA_FAILED - - RESTARTING - - RESIZING - cloud_provider: - type: string - identifier: - type: string - type: - type: string - enum: - - PRIMARY - - READ_REPLICA - disk_volume_size_gb: - type: number - disk_type: - type: string - enum: - - gp3 - - io2 - disk_throughput_mbps: - type: number - disk_last_modified_at: - type: string - required: - - region - - status - - cloud_provider - - identifier - - type - required: - - ref - - name - - cloud_provider - - region - - is_branch - - status - - inserted_at - - databases - pagination: - type: object - properties: - count: - type: number - description: >- - Total number of projects. Use this to calculate the total number - of pages. - limit: - type: number - description: Maximum number of projects per page - offset: - type: number - description: Number of projects skipped in this response - required: - - count - - limit - - offset - required: - - projects - - pagination diff --git a/apps/cli-go/cmd/db.go b/apps/cli-go/cmd/db.go deleted file mode 100644 index bf913d2aa3..0000000000 --- a/apps/cli-go/cmd/db.go +++ /dev/null @@ -1,138 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - - "github.com/spf13/afero" - "github.com/spf13/cobra" - "github.com/spf13/viper" - "github.com/supabase/cli/internal/db/diff" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/internal/utils/flags" - "github.com/supabase/cli/legacy/branch/create" - "github.com/supabase/cli/legacy/branch/delete" - "github.com/supabase/cli/legacy/branch/list" - "github.com/supabase/cli/legacy/branch/switch_" -) - -var ( - dbCmd = &cobra.Command{ - GroupID: groupLocalDev, - Use: "db", - Short: "Manage Postgres databases", - } - - dbBranchCmd = &cobra.Command{ - Hidden: true, - Use: "branch", - Short: "Manage local database branches", - Long: "Manage local database branches. Each branch is associated with a separate local database. Forking remote databases is NOT supported.", - } - - dbBranchCreateCmd = &cobra.Command{ - Deprecated: "use \"branches create \" instead.\n", - Use: "create ", - Short: "Create a branch", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return create.Run(args[0], afero.NewOsFs()) - }, - } - - dbBranchDeleteCmd = &cobra.Command{ - Deprecated: "use \"branches delete \" instead.\n", - Use: "delete ", - Short: "Delete a branch", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return delete.Run(args[0], afero.NewOsFs()) - }, - } - - dbBranchListCmd = &cobra.Command{ - Deprecated: "use \"branches list\" instead.\n", - Use: "list", - Short: "List branches", - RunE: func(cmd *cobra.Command, args []string) error { - return list.Run(afero.NewOsFs(), os.Stdout) - }, - } - - dbSwitchCmd = &cobra.Command{ - Deprecated: "use \"branches create \" instead.\n", - Use: "switch ", - Short: "Switch the active branch", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return switch_.Run(cmd.Context(), args[0], afero.NewOsFs()) - }, - } - - // Bound so the TS `--use-pg-schema` proxy can forward these without unknown-flag errors. - usePgSchema bool - outputPath string - schema []string - file string - dbPassword string - - dbDiffCmd = &cobra.Command{ - Use: "diff", - Short: "Diffs the local database for schema changes", - RunE: func(cmd *cobra.Command, args []string) error { - // TypeScript only proxies `--use-pg-schema` (stripe/pg-schema-diff). - // Other engines run in-process in the TS CLI. - fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "--use-pg-schema flag is experimental and may not include all entities, such as views and grants.") - return diff.Run(cmd.Context(), schema, file, flags.DbConfig, diff.DiffPgSchema, afero.NewOsFs()) - }, - } - - dbRemoteCmd = &cobra.Command{ - Hidden: true, - Use: "remote", - Short: "Manage remote databases", - } - - dbRemoteChangesCmd = &cobra.Command{ - Deprecated: "use \"db diff --use-migra --linked\" instead.\n", - Use: "changes", - Short: "Show changes on the remote database", - Long: "Show changes on the remote database since last migration.", - RunE: func(cmd *cobra.Command, args []string) error { - return diff.Run(cmd.Context(), schema, file, flags.DbConfig, diff.DiffSchemaMigra, afero.NewOsFs()) - }, - } -) - -func init() { - // Build branch command - dbBranchCmd.AddCommand(dbBranchCreateCmd) - dbBranchCmd.AddCommand(dbBranchDeleteCmd) - dbBranchCmd.AddCommand(dbBranchListCmd) - dbBranchCmd.AddCommand(dbSwitchCmd) - dbCmd.AddCommand(dbBranchCmd) - // Build diff command - diffFlags := dbDiffCmd.Flags() - diffFlags.BoolVar(&usePgSchema, "use-pg-schema", false, "Use pg-schema-diff to generate schema diff.") - diffFlags.StringVarP(&outputPath, "output", "o", "", "Write explicit diff output to a file path.") - diffFlags.String("db-url", "", "Diffs against the database specified by the connection string (must be percent-encoded).") - diffFlags.Bool("linked", false, "Diffs local migration files against the linked project.") - diffFlags.Bool("local", true, "Diffs local migration files against the local database.") - dbDiffCmd.MarkFlagsMutuallyExclusive("db-url", "linked", "local") - diffFlags.StringVarP(&file, "file", "f", "", "Saves schema diff to a new migration file.") - diffFlags.StringSliceVarP(&schema, "schema", "s", []string{}, "Comma separated list of schema to include.") - dbCmd.AddCommand(dbDiffCmd) - // Build remote command - remoteFlags := dbRemoteCmd.PersistentFlags() - remoteFlags.StringSliceVarP(&schema, "schema", "s", []string{}, "Comma separated list of schema to include.") - remoteFlags.String("db-url", "", "Connect using the specified Postgres URL (must be percent-encoded).") - remoteFlags.Bool("linked", true, "Connect to the linked project.") - dbRemoteCmd.MarkFlagsMutuallyExclusive("db-url", "linked") - remoteFlags.StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.") - markFlagTelemetrySafe(remoteFlags.Lookup("project-ref")) - remoteFlags.StringVarP(&dbPassword, "password", "p", "", "Password to your remote Postgres database.") - cobra.CheckErr(viper.BindPFlag("DB_PASSWORD", remoteFlags.Lookup("password"))) - dbRemoteCmd.AddCommand(dbRemoteChangesCmd) - dbCmd.AddCommand(dbRemoteCmd) - rootCmd.AddCommand(dbCmd) -} diff --git a/apps/cli-go/cmd/functions.go b/apps/cli-go/cmd/functions.go deleted file mode 100644 index 3434c5ceb8..0000000000 --- a/apps/cli-go/cmd/functions.go +++ /dev/null @@ -1,51 +0,0 @@ -package cmd - -import ( - "github.com/spf13/afero" - "github.com/spf13/cobra" - "github.com/supabase/cli/internal/functions/download" - "github.com/supabase/cli/internal/utils/flags" -) - -var ( - functionsCmd = &cobra.Command{ - GroupID: groupManagementAPI, - Use: "functions", - Short: "Manage Supabase Edge functions", - } - - functionsDownloadCmd = &cobra.Command{ - Use: "download [Function name]", - Short: "Download a Function from Supabase", - Long: "Download the source code for a Function from the linked Supabase project. If no function name is provided, downloads all functions.", - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - if useApi { - useDocker = false - } - slug := "" - if len(args) > 0 { - slug = args[0] - } - return download.Run(cmd.Context(), slug, flags.ProjectRef, useLegacyBundle, useDocker, afero.NewOsFs()) - }, - } - - useApi bool - useDocker bool - useLegacyBundle bool -) - -func init() { - downloadFlags := functionsDownloadCmd.Flags() - downloadFlags.StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.") - markFlagTelemetrySafe(downloadFlags.Lookup("project-ref")) - downloadFlags.BoolVar(&useLegacyBundle, "legacy-bundle", false, "Use legacy bundling mechanism.") - downloadFlags.BoolVar(&useApi, "use-api", false, "Unbundle functions server-side without using Docker.") - downloadFlags.BoolVar(&useDocker, "use-docker", true, "Use Docker to unbundle functions client-side.") - functionsDownloadCmd.MarkFlagsMutuallyExclusive("use-api", "use-docker", "legacy-bundle") - cobra.CheckErr(downloadFlags.MarkHidden("legacy-bundle")) - cobra.CheckErr(downloadFlags.MarkHidden("use-docker")) - functionsCmd.AddCommand(functionsDownloadCmd) - rootCmd.AddCommand(functionsCmd) -} diff --git a/apps/cli-go/cmd/gen.go b/apps/cli-go/cmd/gen.go deleted file mode 100644 index 9ccdbc862d..0000000000 --- a/apps/cli-go/cmd/gen.go +++ /dev/null @@ -1,54 +0,0 @@ -package cmd - -import ( - env "github.com/Netflix/go-env" - "github.com/spf13/afero" - "github.com/spf13/cobra" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/internal/utils/flags" - "github.com/supabase/cli/legacy/keys" -) - -var ( - genCmd = &cobra.Command{ - GroupID: groupLocalDev, - Use: "gen", - Short: "Run code generation tools", - } - - keyNames keys.CustomName - override []string - - genKeysCmd = &cobra.Command{ - Deprecated: `use "gen signing-key" instead.`, - Use: "keys", - Short: "Generate keys for preview branch", - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - es, err := env.EnvironToEnvSet(override) - if err != nil { - return err - } - if err := env.Unmarshal(es, &keyNames); err != nil { - return err - } - cmd.GroupID = groupManagementAPI - return cmd.Root().PersistentPreRunE(cmd, args) - }, - RunE: func(cmd *cobra.Command, args []string) error { - format := utils.OutputFormat.Value - if format == utils.OutputPretty { - format = utils.OutputEnv - } - return keys.Run(cmd.Context(), flags.ProjectRef, format, keyNames, afero.NewOsFs()) - }, - } -) - -func init() { - keyFlags := genKeysCmd.Flags() - keyFlags.StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.") - markFlagTelemetrySafe(keyFlags.Lookup("project-ref")) - keyFlags.StringSliceVar(&override, "override-name", []string{}, "Override specific variable names.") - genCmd.AddCommand(genKeysCmd) - rootCmd.AddCommand(genCmd) -} diff --git a/apps/cli-go/cmd/root.go b/apps/cli-go/cmd/root.go deleted file mode 100644 index 1c48ab558c..0000000000 --- a/apps/cli-go/cmd/root.go +++ /dev/null @@ -1,380 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "net" - "net/http" - "net/url" - "os" - "os/signal" - "strconv" - "strings" - "sync" - "time" - - "github.com/getsentry/sentry-go" - "github.com/go-errors/errors" - "github.com/spf13/afero" - "github.com/spf13/cobra" - "github.com/spf13/viper" - "github.com/supabase/cli/internal/debug" - "github.com/supabase/cli/internal/telemetry" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/internal/utils/flags" - "github.com/supabase/cli/pkg/config" - "golang.org/x/mod/semver" -) - -const ( - groupQuickStart = "quick-start" - groupLocalDev = "local-dev" - groupManagementAPI = "management-api" -) - -func IsManagementAPI(cmd *cobra.Command) bool { - for cmd != cmd.Root() { - if cmd.GroupID == groupManagementAPI { - return true - } - // Find the last assigned group - if len(cmd.GroupID) > 0 { - break - } - cmd = cmd.Parent() - } - return false -} - -func promptLogin(fsys afero.Fs) error { - if _, err := utils.LoadAccessTokenFS(fsys); err == utils.ErrMissingToken { - utils.CmdSuggestion = fmt.Sprintf("Run %s first.", utils.Aqua("supabase login")) - return errors.New("You need to be logged-in in order to use Management API commands.") - } else { - return err - } -} - -var experimental = []*cobra.Command{ - genKeysCmd, -} - -func IsExperimental(cmd *cobra.Command) bool { - for _, exp := range experimental { - if cmd == exp || cmd.Parent() == exp { - return true - } - } - return false -} - -var ( - sentryOpts = sentry.ClientOptions{ - Dsn: utils.SentryDsn, - Release: utils.Version, - ServerName: "", - // Set TracesSampleRate to 1.0 to capture 100% - // of transactions for performance monitoring. - // We recommend adjusting this value in production, - TracesSampleRate: 1.0, - } - - createTicket bool - - rootCmd = &cobra.Command{ - Use: "supabase", - Short: "Supabase CLI " + utils.Version, - Version: utils.Version, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - if IsExperimental(cmd) && !viper.GetBool("EXPERIMENTAL") { - return errors.New("must set the --experimental flag to run this command") - } - cmd.SilenceUsage = true - // Load profile before changing workdir - ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt) - fsys := afero.NewOsFs() - if err := utils.LoadProfile(ctx, fsys); err != nil { - return err - } - if err := utils.ChangeWorkDir(fsys); err != nil { - return err - } - // Add common flags - if IsManagementAPI(cmd) { - if err := promptLogin(fsys); err != nil { - return err - } - if cmd.Flags().Lookup("project-ref") != nil { - if err := flags.ParseProjectRef(ctx, fsys); err != nil { - return err - } - } - } - if err := flags.ParseDatabaseConfig(ctx, cmd.Flags(), fsys); err != nil { - return err - } - // Prepare context - if viper.GetBool("DEBUG") { - http.DefaultTransport = debug.NewTransport() - fmt.Fprintln(os.Stderr, cmd.Root().Short) - fmt.Fprintf(os.Stderr, "Using profile: %s (%s)\n", utils.CurrentProfile.Name, utils.CurrentProfile.ProjectHost) - } - isTTY := telemetryIsTTY() - isCI := telemetryIsCI() - isAgent := telemetryIsAgent() - envSignals := telemetryEnvSignals() - service, err := telemetry.NewService(fsys, telemetry.Options{ - Now: time.Now, - IsTTY: isTTY, - IsCI: isCI, - IsAgent: isAgent, - EnvSignals: envSignals, - CLIName: utils.Version, - }) - if err != nil { - fmt.Fprintln(utils.GetDebugLogger(), err) - } else { - ctx = telemetry.WithService(ctx, service) - } - if service != nil { - var stitchOnce sync.Once - utils.OnGotrueID = func(gotrueID string) { - stitchOnce.Do(func() { - if err := service.ObserveAuthenticatedUser(gotrueID); err != nil { - fmt.Fprintln(utils.GetDebugLogger(), err) - } - }) - } - } - ctx = telemetry.WithCommandContext(ctx, commandAnalyticsContext(cmd)) - cmd.SetContext(ctx) - // Setup sentry last to ignore errors from parsing cli flags - apiHost, err := url.Parse(utils.GetSupabaseAPIHost()) - if err != nil { - return err - } - sentryOpts.Environment = apiHost.Host - return sentry.Init(sentryOpts) - }, - SilenceErrors: true, - } -) - -func Execute() { - defer recoverAndExit() - startedAt := time.Now() - executedCmd, err := rootCmd.ExecuteC() - if executedCmd != nil { - if service := telemetry.FromContext(executedCmd.Context()); service != nil { - ensureProjectGroupsCached(executedCmd.Context(), service) - _ = service.Capture(executedCmd.Context(), telemetry.EventCommandExecuted, map[string]any{ - telemetry.PropExitCode: exitCode(err), - telemetry.PropDurationMs: time.Since(startedAt).Milliseconds(), - telemetry.PropOutputFormat: utils.OutputFormat.Value, - }, nil) - _ = service.Close() - } - } - if err != nil { - panic(err) - } - // Check upgrade last because --version flag is initialised after execute - ctx := rootCmd.Context() - if executedCmd != nil { - ctx = executedCmd.Context() - } - if hint := utils.SuggestClaudePlugin(); hint != "" { - fmt.Fprintln(os.Stderr, hint) - } - if updateNotifierEnabled() { - version, err := checkUpgrade(ctx, afero.NewOsFs()) - if err != nil { - fmt.Fprintln(utils.GetDebugLogger(), err) - } - if semver.Compare(version, "v"+utils.Version) > 0 { - fmt.Fprintln(os.Stderr, suggestUpgrade(version)) - } - } - if len(utils.CmdSuggestion) > 0 { - fmt.Fprintln(os.Stderr, utils.CmdSuggestion) - } -} - -// ensureProjectGroupsCached populates the telemetry linked-project cache when -// a project ref is available but no cache exists. This ensures org/project -// PostHog groups are attached to all CLI events, not just those after `supabase link`. -// -// Does not overwrite an existing cache — `supabase link` is the authoritative source. -// Checks auth before calling the API to avoid the log.Fatalln in GetSupabase(). -func ensureProjectGroupsCached(ctx context.Context, service *telemetry.Service) { - ref := flags.ProjectRef - if ref == "" || config.UsesStackState() { - return - } - fsys := afero.NewOsFs() - if telemetry.HasLinkedProject(fsys) { - return - } - if _, err := utils.LoadAccessTokenFS(fsys); err != nil { - return - } - resp, err := utils.GetSupabase().V1GetProjectWithResponse(ctx, ref) - if err != nil { - fmt.Fprintln(utils.GetDebugLogger(), err) - return - } - if resp.JSON200 == nil { - return - } - telemetry.CacheProjectAndIdentifyGroups(*resp.JSON200, service, fsys) -} - -func exitCode(err error) int { - if err != nil { - return 1 - } - return 0 -} - -func updateNotifierEnabled() bool { - // Read the env directly instead of viper: the upgrade check also runs for - // --help and --version, which skip the cobra initializer that binds env vars. - disabled, _ := strconv.ParseBool(os.Getenv("SUPABASE_NO_UPDATE_NOTIFIER")) - return !disabled && !config.UsesStackState() -} - -func checkUpgrade(ctx context.Context, fsys afero.Fs) (string, error) { - if shouldFetchRelease(fsys) { - version, err := utils.GetLatestRelease(ctx) - if exists, _ := afero.DirExists(fsys, utils.SupabaseDirPath); exists { - // If user is offline, write an empty file to skip subsequent checks - err = utils.WriteFile(utils.CliVersionPath, []byte(version), fsys) - } - return version, err - } - version, err := afero.ReadFile(fsys, utils.CliVersionPath) - if err != nil { - return "", errors.Errorf("failed to read cli version: %w", err) - } - return string(version), nil -} - -func shouldFetchRelease(fsys afero.Fs) bool { - // Always fetch latest release when using --version flag - if vf := rootCmd.Flag("version"); vf != nil && vf.Changed { - return true - } - if fi, err := fsys.Stat(utils.CliVersionPath); err == nil { - expiry := fi.ModTime().Add(time.Hour * 10) - // Skip if last checked is less than 10 hours ago - return time.Now().After(expiry) - } - return true -} - -func suggestUpgrade(version string) string { - const guide = "https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli" - return fmt.Sprintf(`A new version of Supabase CLI is available: %s (currently installed v%s) -We recommend updating regularly for new features and bug fixes: %s`, utils.Yellow(version), utils.Version, utils.Bold(guide)) -} - -func recoverAndExit() { - err := recover() - if err == nil { - return - } - var msg string - switch err := err.(type) { - case string: - msg = err - case error: - if !errors.Is(err, context.Canceled) && - len(utils.CmdSuggestion) == 0 && - !viper.GetBool("DEBUG") { - utils.CmdSuggestion = utils.SuggestDebugFlag - } - if e, ok := err.(*errors.Error); ok && len(utils.Version) == 0 { - fmt.Fprintln(os.Stderr, string(e.Stack())) - } - msg = err.Error() - default: - msg = fmt.Sprintf("%#v", err) - } - // Log error to console - fmt.Fprintln(os.Stderr, utils.Red(msg)) - if len(utils.CmdSuggestion) > 0 { - fmt.Fprintln(os.Stderr, utils.CmdSuggestion) - } - // Report error to sentry - if createTicket && len(utils.SentryDsn) > 0 { - sentry.ConfigureScope(addSentryScope) - eventId := sentry.CurrentHub().Recover(err) - if eventId != nil && sentry.Flush(2*time.Second) { - fmt.Fprintln(os.Stderr, "Sent crash report:", *eventId) - fmt.Fprintln(os.Stderr, "Quote the crash ID above when filing a bug report: https://github.com/supabase/cli/issues/new/choose") - } - } - os.Exit(1) -} - -func init() { - cobra.OnInitialize(func() { - viper.SetEnvPrefix("SUPABASE") - viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) - viper.AutomaticEnv() - }) - - // Shell tab-completion is fully native in the TS shim now (CLI-1965); the - // TS entrypoint intercepts completion/__complete/__completeNoDesc before - // ever delegating to this binary, so cobra's own completion command is - // unreachable dead weight here. This only removes the visible - // `completion ` command — cobra's ExecuteC() unconditionally - // (re-)registers the hidden __complete/__completeNoDesc responder on - // every run with no opt-out (command.go's initCompleteCmd), so that - // protocol handler stays present but, same as above, unreachable through - // the shipped CLI. - rootCmd.CompletionOptions.DisableDefaultCmd = true - - flags := rootCmd.PersistentFlags() - flags.Bool("yes", false, "answer yes to all prompts") - flags.Bool("debug", false, "output debug logs to stderr") - flags.String("workdir", "", "path to a Supabase project directory") - flags.Bool("experimental", false, "enable experimental features") - flags.String("network-id", "", "use the specified docker network instead of a generated one") - flags.String("profile", "supabase", "use a specific profile for connecting to Supabase API") - flags.VarP(&utils.OutputFormat, "output", "o", "output format of status variables") - flags.Var(&utils.DNSResolver, "dns-resolver", "lookup domain names using the specified resolver") - flags.BoolVar(&createTicket, "create-ticket", false, "create a support ticket for any CLI error") - flags.VarP(&utils.AgentMode, "agent", "", "Override agent detection: yes, no, or auto (default auto)") - cobra.CheckErr(viper.BindPFlags(flags)) - - rootCmd.SetVersionTemplate("{{.Version}}\n") - rootCmd.AddGroup(&cobra.Group{ID: groupQuickStart, Title: "Quick Start:"}) - rootCmd.AddGroup(&cobra.Group{ID: groupLocalDev, Title: "Local Development:"}) - rootCmd.AddGroup(&cobra.Group{ID: groupManagementAPI, Title: "Management APIs:"}) -} - -// instantiate new rootCmd is a bit tricky with cobra, but it can be done later with the following -// approach for example: https://github.com/portworx/pxc/tree/master/cmd -func GetRootCmd() *cobra.Command { - return rootCmd -} - -func addSentryScope(scope *sentry.Scope) { - serviceImages := utils.Config.GetServiceImages() - imageToVersion := make(map[string]any, len(serviceImages)) - for _, image := range serviceImages { - parts := strings.Split(image, ":") - // Bypasses sentry's IP sanitization rule, ie. 15.1.0.147 - if net.ParseIP(parts[1]) != nil { - imageToVersion[parts[0]] = "v" + parts[1] - } else { - imageToVersion[parts[0]] = parts[1] - } - } - scope.SetContext("Services", imageToVersion) - scope.SetContext("Config", map[string]any{ - "Image Registry": utils.GetRegistry(), - "Project ID": flags.ProjectRef, - }) -} diff --git a/apps/cli-go/cmd/root_analytics.go b/apps/cli-go/cmd/root_analytics.go deleted file mode 100644 index bf9735ec3c..0000000000 --- a/apps/cli-go/cmd/root_analytics.go +++ /dev/null @@ -1,180 +0,0 @@ -package cmd - -import ( - "os" - "sort" - "strconv" - "strings" - - "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/supabase/cli/internal/telemetry" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/internal/utils/agent" - "golang.org/x/term" -) - -const ( - telemetrySafeValueAnnotation = "supabase.com/telemetry-safe-value" - redactedTelemetryValue = "" - maxTelemetryEnvValueLength = 80 -) - -func commandAnalyticsContext(cmd *cobra.Command) telemetry.CommandContext { - return telemetry.CommandContext{ - RunID: uuid.NewString(), - Command: commandName(cmd), - Flags: changedFlagValues(cmd), - } -} - -func commandName(cmd *cobra.Command) string { - path := strings.TrimSpace(cmd.CommandPath()) - rootName := strings.TrimSpace(cmd.Root().Name()) - if path == rootName || path == "" { - return rootName - } - return strings.TrimSpace(strings.TrimPrefix(path, rootName)) -} - -func changedFlagValues(cmd *cobra.Command) map[string]any { - flags := changedFlags(cmd) - if len(flags) == 0 { - return nil - } - values := make(map[string]any, len(flags)) - for _, flag := range flags { - values[flag.Name] = telemetryFlagValue(flag) - } - return values -} - -func changedFlags(cmd *cobra.Command) []*pflag.Flag { - seen := make(map[string]struct{}) - var result []*pflag.Flag - collect := func(flags *pflag.FlagSet) { - if flags == nil { - return - } - flags.Visit(func(flag *pflag.Flag) { - if _, ok := seen[flag.Name]; ok { - return - } - seen[flag.Name] = struct{}{} - result = append(result, flag) - }) - } - for current := cmd; current != nil; current = current.Parent() { - collect(current.PersistentFlags()) - } - collect(cmd.Flags()) - sort.Slice(result, func(i, j int) bool { - return result[i].Name < result[j].Name - }) - return result -} - -func markFlagTelemetrySafe(flag *pflag.Flag) { - if flag == nil { - return - } - if flag.Annotations == nil { - flag.Annotations = map[string][]string{} - } - flag.Annotations[telemetrySafeValueAnnotation] = []string{"true"} -} - -func telemetryFlagValue(flag *pflag.Flag) any { - if flag == nil { - return nil - } - if isTelemetrySafeFlag(flag) || isBooleanFlag(flag) || isEnumFlag(flag) { - return actualTelemetryFlagValue(flag) - } - return redactedTelemetryValue -} - -func isTelemetrySafeFlag(flag *pflag.Flag) bool { - if flag == nil || flag.Annotations == nil { - return false - } - values, ok := flag.Annotations[telemetrySafeValueAnnotation] - return ok && len(values) > 0 && values[0] == "true" -} - -func isBooleanFlag(flag *pflag.Flag) bool { - return flag != nil && flag.Value.Type() == "bool" -} - -func isEnumFlag(flag *pflag.Flag) bool { - if flag == nil { - return false - } - _, ok := flag.Value.(*utils.EnumFlag) - return ok -} - -func actualTelemetryFlagValue(flag *pflag.Flag) any { - if isBooleanFlag(flag) { - value, err := strconv.ParseBool(flag.Value.String()) - if err == nil { - return value - } - } - return flag.Value.String() -} - -func telemetryIsCI() bool { - return os.Getenv("CI") != "" || - os.Getenv("GITHUB_ACTIONS") != "" || - os.Getenv("BUILDKITE") != "" || - os.Getenv("TF_BUILD") != "" || - os.Getenv("JENKINS_URL") != "" || - os.Getenv("GITLAB_CI") != "" -} - -func telemetryIsTTY() bool { - return term.IsTerminal(int(os.Stdout.Fd())) //nolint:gosec // G115: stdout fd is a small int on supported platforms -} - -func telemetryIsAgent() bool { - return agent.IsAgent() -} - -func telemetryEnvSignals() map[string]any { - return envSignals(telemetry.EnvSignalPresenceKeys[:], telemetry.EnvSignalValueKeys[:]) -} - -func envSignals(presenceKeys []string, valueKeys []string) map[string]any { - signals := make(map[string]any, len(presenceKeys)+len(valueKeys)) - for _, key := range presenceKeys { - if hasTelemetryEnvValue(key) { - signals[key] = true - } - } - for _, key := range valueKeys { - if value := telemetryEnvValue(key); value != "" { - signals[key] = value - } - } - if len(signals) == 0 { - return nil - } - return signals -} - -func hasTelemetryEnvValue(key string) bool { - return strings.TrimSpace(os.Getenv(key)) != "" -} - -func telemetryEnvValue(key string) string { - value := strings.TrimSpace(os.Getenv(key)) - if value == "" { - return "" - } - if len(value) > maxTelemetryEnvValueLength { - return value[:maxTelemetryEnvValueLength] - } - return value -} diff --git a/apps/cli-go/cmd/root_test.go b/apps/cli-go/cmd/root_test.go deleted file mode 100644 index 8dc63afd48..0000000000 --- a/apps/cli-go/cmd/root_test.go +++ /dev/null @@ -1,144 +0,0 @@ -package cmd - -import ( - "strings" - "testing" - - "github.com/spf13/cobra" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/telemetry" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/config" -) - -func clearTelemetryEnv(t *testing.T) { - for _, key := range telemetry.EnvSignalPresenceKeys { - t.Setenv(key, "") - } - for _, key := range telemetry.EnvSignalValueKeys { - t.Setenv(key, "") - } -} - -func TestCommandAnalyticsContext(t *testing.T) { - root := &cobra.Command{Use: "supabase"} - var projectRef string - var password string - var debug bool - output := utils.EnumFlag{ - Allowed: []string{"json", "table"}, - Value: "table", - } - child := &cobra.Command{ - Use: "link", - RunE: func(cmd *cobra.Command, args []string) error { - return nil - }, - } - root.PersistentFlags().BoolVar(&debug, "debug", false, "") - child.Flags().StringVar(&projectRef, "project-ref", "", "") - child.Flags().StringVar(&password, "password", "", "") - child.Flags().Var(&output, "output", "") - child.Flags().AddFlag(root.PersistentFlags().Lookup("debug")) - markFlagTelemetrySafe(child.Flags().Lookup("project-ref")) - root.AddCommand(child) - - require.NoError(t, root.PersistentFlags().Set("debug", "true")) - require.NoError(t, child.Flags().Set("project-ref", "proj_123")) - require.NoError(t, child.Flags().Set("password", "hunter2")) - require.NoError(t, child.Flags().Set("output", "json")) - - ctx := commandAnalyticsContext(child) - - assert.Equal(t, "link", ctx.Command) - assert.Equal(t, map[string]any{ - "debug": true, - "output": "json", - "password": redactedTelemetryValue, - "project-ref": "proj_123", - }, ctx.Flags) - assert.NotContains(t, ctx.Flags, "linked") - assert.NotEmpty(t, ctx.RunID) -} - -func TestCommandName(t *testing.T) { - root := &cobra.Command{Use: "supabase"} - parent := &cobra.Command{Use: "db"} - child := &cobra.Command{Use: "push"} - root.AddCommand(parent) - parent.AddCommand(child) - - assert.Equal(t, "db push", commandName(child)) - assert.Equal(t, "supabase", commandName(root)) -} - -func TestTelemetryIsAgent(t *testing.T) { - t.Run("returns true for agent env", func(t *testing.T) { - clearTelemetryEnv(t) - t.Setenv("CLAUDE_CODE", "1") - utils.AgentMode.Value = "auto" - t.Cleanup(func() { - utils.AgentMode.Value = "auto" - }) - - assert.True(t, telemetryIsAgent()) - }) - - t.Run("returns false with no agent env", func(t *testing.T) { - clearTelemetryEnv(t) - utils.AgentMode.Value = "auto" - t.Cleanup(func() { - utils.AgentMode.Value = "auto" - }) - - assert.False(t, telemetryIsAgent()) - }) -} - -func TestTelemetryEnvSignals(t *testing.T) { - clearTelemetryEnv(t) - t.Setenv("CURSOR_AGENT", "1") - t.Setenv("TERM_PROGRAM", " iTerm.app ") - - signals := telemetryEnvSignals() - - assert.Equal(t, true, signals["CURSOR_AGENT"]) - assert.Equal(t, "iTerm.app", signals["TERM_PROGRAM"]) - assert.NotContains(t, signals, "AI_AGENT") -} - -func TestEnvSignals(t *testing.T) { - clearTelemetryEnv(t) - t.Setenv("AI_AGENT", " ") - t.Setenv("TERM_PROGRAM", " iTerm.app ") - t.Setenv("TERM", strings.Repeat("x", 100)) - - signals := envSignals([]string{"AI_AGENT"}, []string{"TERM_PROGRAM", "TERM"}) - - assert.Equal(t, "iTerm.app", signals["TERM_PROGRAM"]) - assert.Equal(t, strings.Repeat("x", 80), signals["TERM"]) - assert.NotContains(t, signals, "AI_AGENT") -} - -func TestUpdateNotifierEnabled(t *testing.T) { - for value, wantEnabled := range map[string]bool{ - "": true, - "0": true, - "false": true, - "garbage": true, - "1": false, - "true": false, - "TRUE": false, - "t": false, - } { - t.Setenv("SUPABASE_NO_UPDATE_NOTIFIER", value) - assert.Equal(t, wantEnabled, updateNotifierEnabled(), "value %q", value) - } -} - -func TestUpdateNotifierDisabledInStackMode(t *testing.T) { - t.Setenv("SUPABASE_NO_UPDATE_NOTIFIER", "") - t.Setenv(config.StackStateEnv, "1") - assert.False(t, updateNotifierEnabled()) -} diff --git a/apps/cli-go/fsevents/fsevents.go b/apps/cli-go/fsevents/fsevents.go deleted file mode 100644 index d75d319324..0000000000 --- a/apps/cli-go/fsevents/fsevents.go +++ /dev/null @@ -1,117 +0,0 @@ -//go:build darwin - -// Package fsevents provides file system notifications on macOS. -package fsevents - -import ( - "syscall" - "time" -) - -// Event represents a single file system notification. -type Event struct { - // Path holds the path to the item that's changed, relative - // to its device's root. - // Use DeviceForPath to determine the absolute path that's - // being referred to. - Path string - - // Flags holds details what has happened. - Flags EventFlags - - // ID holds the event ID. - // - // Each event ID comes from the most recent event being reported - // in the corresponding directory named in the EventStream.Paths field - // Event IDs all come from a single global source. - // They are guaranteed to always be increasing, usually in leaps - // and bounds, even across system reboots and moving drives from - // one machine to another. If you were to - // stop processing events from this stream after this event - // and resume processing them later from a newly-created - // EventStream, this is the value you would pass for the - // EventStream.EventID along with Resume=true. - ID uint64 -} - -// DeviceForPath returns the device ID for the specified volume. -func DeviceForPath(path string) (int32, error) { - stat := syscall.Stat_t{} - if err := syscall.Lstat(path, &stat); err != nil { - return 0, err - } - return stat.Dev, nil -} - -// EventStream is the primary interface to FSEvents -// You can provide your own event channel if you wish (or one will be -// created on Start). -// -// es := &EventStream{Paths: []string{"/tmp"}, Flags: 0} -// es.Start() -// es.Stop() -// ... -type EventStream struct { - // Events holds the channel on which events will be sent. - // It's initialized by EventStream.Start if nil. - Events chan []Event - - // Paths holds the set of paths to watch, each - // specifying the root of a filesystem hierarchy to be - // watched for modifications. - Paths []string - - // Flags specifies what events to receive on the stream. - Flags CreateFlags - - // Resume specifies that watching should resume from the event - // specified by EventID. - Resume bool - - // EventID holds the most recent event ID. - // - // NOTE: this is updated asynchronously by the - // watcher and should not be accessed while - // the stream has been started. - EventID uint64 - - // Latency holds the number of seconds the service should wait after hearing - // about an event from the kernel before passing it along to the - // client via its callback. Specifying a larger value may result - // in more effective temporal coalescing, resulting in fewer - // callbacks and greater overall efficiency. - Latency time.Duration - - // When Device is non-zero, the watcher will watch events on the - // device with this ID, and the paths in the Paths field are - // interpreted relative to the device's root. - // - // The device ID is the same as the st_dev field from a stat - // structure of a file on that device or the f_fsid[0] field of - // a statfs structure. - Device int32 -} - -// Start listening to an event stream. This creates es.Events if it's not already -// a valid channel. -func (es *EventStream) Start() error { - return nil -} - -// Flush flushes events that have occurred but haven't been delivered. -// If sync is true, it will block until all the events have been delivered, -// otherwise it will return immediately. -func (es *EventStream) Flush(sync bool) { -} - -// Stop stops listening to the event stream. -func (es *EventStream) Stop() { -} - -// Restart restarts the event listener. This -// can be used to change the current watch flags. -func (es *EventStream) Restart() error { - es.Stop() - es.Resume = true - return es.Start() -} diff --git a/apps/cli-go/fsevents/go.mod b/apps/cli-go/fsevents/go.mod deleted file mode 100644 index 41e670c561..0000000000 --- a/apps/cli-go/fsevents/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -go 1.24.10 - -module github.com/fsnotify/fsevents diff --git a/apps/cli-go/fsevents/wrap.go b/apps/cli-go/fsevents/wrap.go deleted file mode 100644 index f12125b7e3..0000000000 --- a/apps/cli-go/fsevents/wrap.go +++ /dev/null @@ -1,40 +0,0 @@ -//go:build darwin - -package fsevents - -// CreateFlags specifies what events will be seen in an event stream. -type CreateFlags uint32 - -const ( - // IgnoreSelf doesn't send events triggered by the current process (macOS 10.6+). - // - // Don't send events that were triggered by the current process. - // This is useful for reducing the volume of events that are - // sent. It is only useful if your process might modify the file - // system hierarchy beneath the path(s) being monitored. Note: - // this has no effect on historical events, i.e., those - // delivered before the HistoryDone sentinel event. - IgnoreSelf = CreateFlags(0) - - // FileEvents sends events about individual files, generating significantly - // more events (macOS 10.7+) than directory level notifications. - FileEvents = CreateFlags(1) -) - -// EventFlags passed to the FSEventStreamCallback function. -// These correspond directly to the flags as described here: -// https://developer.apple.com/documentation/coreservices/1455361-fseventstreameventflags -type EventFlags uint32 - -const ( - // ItemCreated indicates that a file or directory has been created. - ItemCreated = EventFlags(0) - - // ItemIsDir indicates that the item is a directory. - ItemIsDir = EventFlags(1) -) - -// LatestEventID returns the most recently generated event ID, system-wide. -func LatestEventID() uint64 { - return 0 -} diff --git a/apps/cli-go/go.mod b/apps/cli-go/go.mod deleted file mode 100644 index add818204b..0000000000 --- a/apps/cli-go/go.mod +++ /dev/null @@ -1,379 +0,0 @@ -module github.com/supabase/cli - -go 1.26.0 - -require ( - github.com/BurntSushi/toml v1.6.0 - github.com/Netflix/go-env v0.1.2 - github.com/andybalholm/brotli v1.2.3 - github.com/cenkalti/backoff/v4 v4.3.0 - github.com/charmbracelet/bubbles v1.0.0 - github.com/charmbracelet/bubbletea v1.3.10 - github.com/charmbracelet/glamour v1.0.0 - github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 - github.com/compose-spec/compose-go/v2 v2.9.1 - github.com/containerd/errdefs v1.0.0 - github.com/containers/common v0.64.2 - github.com/docker/cli v28.5.2+incompatible - github.com/docker/docker v28.5.2+incompatible - github.com/docker/go-connections v0.8.1 - github.com/docker/go-units v0.5.0 - github.com/getsentry/sentry-go v0.49.0 - github.com/go-errors/errors v1.5.1 - github.com/go-git/go-git/v5 v5.19.2 - github.com/go-playground/validator/v10 v10.30.4 - github.com/go-viper/mapstructure/v2 v2.5.0 - github.com/google/go-github/v62 v62.0.0 - github.com/google/go-querystring v1.2.0 - github.com/google/uuid v1.6.0 - github.com/h2non/gock v1.2.0 - github.com/jackc/pgconn v1.14.3 - github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 - github.com/jackc/pgproto3/v2 v2.3.3 - github.com/jackc/pgx/v4 v4.18.3 - github.com/joho/godotenv v1.5.1 - github.com/muesli/reflow v0.3.0 - github.com/oapi-codegen/nullable v1.2.0 - github.com/olekukonko/tablewriter v1.1.4 - github.com/posthog/posthog-go v1.25.1 - github.com/spf13/afero v1.15.0 - github.com/spf13/cobra v1.10.2 - github.com/spf13/pflag v1.0.10 - github.com/spf13/viper v1.21.0 - github.com/stretchr/testify v1.12.1 - github.com/stripe/pg-schema-diff v1.0.9 - github.com/supabase/cli/pkg v1.0.0 - github.com/zalando/go-keyring v0.2.8 - go.opentelemetry.io/otel v1.46.0 - golang.org/x/mod v0.41.0 - golang.org/x/net v0.58.0 - golang.org/x/oauth2 v0.37.0 - golang.org/x/term v0.45.0 - google.golang.org/grpc v1.83.2 - gopkg.in/yaml.v3 v3.0.1 -) - -require ( - 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect - 4d63.com/gochecknoglobals v0.2.2 // indirect - charm.land/lipgloss/v2 v2.0.3 // indirect - codeberg.org/chavacava/garif v0.2.0 // indirect - codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect - dario.cat/mergo v1.0.2 // indirect - dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect - dev.gaijin.team/go/golib v0.6.0 // indirect - github.com/4meepo/tagalign v1.4.3 // indirect - github.com/Abirdcfly/dupword v0.1.7 // indirect - github.com/AdminBenni/iota-mixing v1.0.0 // indirect - github.com/AlwxSin/noinlineerr v1.0.5 // indirect - github.com/Antonboom/errname v1.1.1 // indirect - github.com/Antonboom/nilnil v1.1.1 // indirect - github.com/Antonboom/testifylint v1.6.4 // indirect - github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect - github.com/ClickHouse/clickhouse-go-linter v1.2.0 // indirect - github.com/Djarvur/go-err113 v0.1.1 // indirect - github.com/Masterminds/semver/v3 v3.5.0 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/MirrexOne/unqueryvet v1.5.4 // indirect - github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect - github.com/ProtonMail/go-crypto v1.3.0 // indirect - github.com/alecthomas/chroma/v2 v2.24.1 // indirect - github.com/alecthomas/go-check-sumtype v0.3.1 // indirect - github.com/alexkohler/nakedret/v2 v2.0.6 // indirect - github.com/alexkohler/prealloc v1.1.0 // indirect - github.com/alfatraining/structtag v1.0.0 // indirect - github.com/alingse/asasalint v0.0.11 // indirect - github.com/alingse/nilnesserr v0.2.0 // indirect - github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect - github.com/ashanbrown/forbidigo/v2 v2.3.1 // indirect - github.com/ashanbrown/makezero/v2 v2.2.1 // indirect - github.com/atotto/clipboard v0.1.4 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/aymerick/douceur v0.2.0 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/bitfield/gotestdox v0.2.2 // indirect - github.com/bkielbasa/cyclop v1.2.3 // indirect - github.com/blizzy78/varnamelen v0.8.0 // indirect - github.com/bombsimon/wsl/v4 v4.7.0 // indirect - github.com/bombsimon/wsl/v5 v5.8.0 // indirect - github.com/breml/bidichk v0.3.3 // indirect - github.com/breml/errchkjson v0.4.1 // indirect - github.com/butuzov/ireturn v0.4.1 // indirect - github.com/butuzov/mirror v1.3.0 // indirect - github.com/catenacyber/perfsprint v0.10.1 // indirect - github.com/ccojocar/zxcvbn-go v1.0.4 // indirect - github.com/cenkalti/backoff/v5 v5.0.3 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/charithe/durationcheck v0.0.11 // indirect - github.com/charmbracelet/colorprofile v0.4.3 // indirect - github.com/charmbracelet/harmonica v0.2.0 // indirect - github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 // indirect - github.com/charmbracelet/x/ansi v0.11.7 // indirect - github.com/charmbracelet/x/cellbuf v0.0.15 // indirect - github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect - github.com/charmbracelet/x/term v0.2.2 // indirect - github.com/charmbracelet/x/termios v0.1.1 // indirect - github.com/charmbracelet/x/windows v0.2.2 // indirect - github.com/ckaznocha/intrange v0.3.1 // indirect - github.com/clipperhouse/displaywidth v0.11.0 // indirect - github.com/clipperhouse/uax29/v2 v2.7.0 // indirect - github.com/cloudflare/circl v1.6.3 // indirect - github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/containers/storage v1.59.1 // indirect - github.com/creack/pty v1.1.24 // indirect - github.com/curioswitch/go-reassign v0.3.0 // indirect - github.com/cyphar/filepath-securejoin v0.6.1 // indirect - github.com/daixiang0/gci v0.13.7 // indirect - github.com/danieljoos/wincred v1.2.3 // indirect - github.com/dave/dst v0.27.3 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect - github.com/denis-tingaikin/go-header v0.5.0 // indirect - github.com/distribution/reference v0.6.0 // indirect - github.com/dlclark/regexp2 v1.12.0 // indirect - github.com/dnephin/pflag v1.0.7 // indirect - github.com/docker/docker-credential-helpers v0.9.5 // indirect - github.com/docker/go-metrics v0.0.1 // indirect - github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect - github.com/ecies/go/v2 v2.0.11 // indirect - github.com/emirpasic/gods v1.18.1 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/ethereum/go-ethereum v1.17.0 // indirect - github.com/ettle/strcase v0.2.0 // indirect - github.com/fatih/color v1.19.0 // indirect - github.com/fatih/structtag v1.2.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/firefart/nonamedreturns v1.0.6 // indirect - github.com/fsnotify/fsnotify v1.10.1 // indirect - github.com/fvbommel/sortorder v1.1.0 // indirect - github.com/fzipp/gocyclo v0.6.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.15 // indirect - github.com/getkin/kin-openapi v0.144.0 // indirect - github.com/ghostiam/protogetter v0.3.20 // indirect - github.com/go-critic/go-critic v0.14.3 // indirect - github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.9.0 // indirect - github.com/go-logr/logr v1.4.4 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.22.5 // indirect - github.com/go-openapi/swag/jsonname v0.25.5 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-toolsmith/astcast v1.1.0 // indirect - github.com/go-toolsmith/astcopy v1.1.0 // indirect - github.com/go-toolsmith/astequal v1.2.0 // indirect - github.com/go-toolsmith/astfmt v1.1.0 // indirect - github.com/go-toolsmith/astp v1.1.0 // indirect - github.com/go-toolsmith/strparse v1.1.0 // indirect - github.com/go-toolsmith/typep v1.1.0 // indirect - github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect - github.com/gobwas/glob v0.2.3 // indirect - github.com/goccy/go-json v0.10.6 // indirect - github.com/godbus/dbus/v5 v5.2.2 // indirect - github.com/godoc-lint/godoc-lint v0.11.2 // indirect - github.com/gofrs/flock v0.13.0 // indirect - github.com/golang-jwt/jwt/v5 v5.3.1 // indirect - github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect - github.com/golangci/asciicheck v0.5.0 // indirect - github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 // indirect - github.com/golangci/go-printf-func-name v0.1.1 // indirect - github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect - github.com/golangci/golangci-lint/v2 v2.12.2 // indirect - github.com/golangci/golines v0.15.0 // indirect - github.com/golangci/misspell v0.8.0 // indirect - github.com/golangci/plugin-module-register v0.1.2 // indirect - github.com/golangci/revgrep v0.8.0 // indirect - github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba // indirect - github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect - github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/gordonklaus/ineffassign v0.2.0 // indirect - github.com/gorilla/css v1.0.1 // indirect - github.com/gostaticanalysis/analysisutil v0.7.1 // indirect - github.com/gostaticanalysis/comment v1.5.0 // indirect - github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect - github.com/gostaticanalysis/nilerr v0.1.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect - github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect - github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect - github.com/hashicorp/go-version v1.9.0 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect - github.com/hexops/gotextdiff v1.0.3 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgio v1.0.0 // indirect - github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgtype v1.14.4 // indirect - github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/jgautheron/goconst v1.10.0 // indirect - github.com/jjti/go-spancheck v0.6.5 // indirect - github.com/julz/importas v0.2.0 // indirect - github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect - github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/kisielk/errcheck v1.10.0 // indirect - github.com/kkHAIKE/contextcheck v1.1.6 // indirect - github.com/klauspost/compress v1.18.5 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/kr/pretty v0.3.1 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/kulti/thelper v0.7.1 // indirect - github.com/kunwardeep/paralleltest v1.0.15 // indirect - github.com/lasiar/canonicalheader v1.1.2 // indirect - github.com/ldez/exptostd v0.4.5 // indirect - github.com/ldez/gomoddirectives v0.8.0 // indirect - github.com/ldez/grignotin v0.10.1 // indirect - github.com/ldez/structtags v0.6.1 // indirect - github.com/ldez/tagliatelle v0.7.2 // indirect - github.com/ldez/usetesting v0.5.0 // indirect - github.com/leodido/go-urn v1.5.0 // indirect - github.com/leonklingele/grouper v1.1.2 // indirect - github.com/lib/pq v1.12.3 // indirect - github.com/lucasb-eyer/go-colorful v1.4.0 // indirect - github.com/macabu/inamedparam v0.2.0 // indirect - github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect - github.com/manuelarte/funcorder v0.6.0 // indirect - github.com/maratori/testableexamples v1.0.1 // indirect - github.com/maratori/testpackage v1.1.2 // indirect - github.com/matoous/godox v1.1.0 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.23 // indirect - github.com/mattn/go-shellwords v1.0.12 // indirect - github.com/mgechev/revive v1.15.0 // indirect - github.com/microcosm-cc/bluemonday v1.0.27 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect - github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/sys/atomicwriter v0.1.0 // indirect - github.com/moby/sys/sequential v0.6.0 // indirect - github.com/moby/term v0.5.2 // indirect - github.com/moricho/tparallel v0.3.2 // indirect - github.com/morikuni/aec v1.1.0 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect - github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.16.0 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/nakabonne/nestif v0.3.1 // indirect - github.com/nishanths/exhaustive v0.12.0 // indirect - github.com/nishanths/predeclared v0.2.2 // indirect - github.com/nunnatsa/ginkgolinter v0.23.0 // indirect - github.com/oapi-codegen/oapi-codegen/v2 v2.7.1 // indirect - github.com/oapi-codegen/runtime v1.7.0 // indirect - github.com/oasdiff/yaml v0.1.1 // indirect - github.com/oasdiff/yaml3 v0.0.14 // indirect - github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect - github.com/olekukonko/errors v1.2.0 // indirect - github.com/olekukonko/ll v0.1.6 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/otiai10/copy v1.14.1 // indirect - github.com/pelletier/go-toml/v2 v2.3.1 // indirect - github.com/pjbgf/sha1cd v0.6.0 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_golang v1.23.2 // indirect - github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.4 // indirect - github.com/prometheus/procfs v0.19.2 // indirect - github.com/quasilyte/go-ruleguard v0.4.5 // indirect - github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect - github.com/quasilyte/gogrep v0.5.0 // indirect - github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect - github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect - github.com/raeperd/recvcheck v0.2.0 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/ryancurrah/gomodguard v1.4.1 // indirect - github.com/ryancurrah/gomodguard/v2 v2.1.3 // indirect - github.com/ryanrolds/sqlclosecheck v0.6.0 // indirect - github.com/sagikazarmark/locafero v0.12.0 // indirect - github.com/sahilm/fuzzy v0.1.1 // indirect - github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect - github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect - github.com/sashamelentyev/interfacebloat v1.1.0 // indirect - github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect - github.com/securego/gosec/v2 v2.26.1 // indirect - github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/sirupsen/logrus v1.9.4 // indirect - github.com/sivchari/containedctx v1.0.3 // indirect - github.com/skeema/knownhosts v1.3.1 // indirect - github.com/sonatard/noctx v0.5.1 // indirect - github.com/sourcegraph/go-diff v0.8.0 // indirect - github.com/speakeasy-api/jsonpath v0.6.3 // indirect - github.com/speakeasy-api/openapi v1.19.2 // indirect - github.com/spf13/cast v1.10.0 // indirect - github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect - github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect - github.com/stretchr/objx v0.5.3 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - github.com/tetafro/godot v1.5.6 // indirect - github.com/theupdateframework/notary v0.7.0 // indirect - github.com/tidwall/jsonc v0.3.3 // indirect - github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4 // indirect - github.com/timonwong/loggercheck v0.11.0 // indirect - github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect - github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect - github.com/ultraware/funlen v0.2.0 // indirect - github.com/ultraware/whitespace v0.2.0 // indirect - github.com/uudashr/gocognit v1.2.1 // indirect - github.com/uudashr/iface v1.4.2 // indirect - github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect - github.com/xanzy/ssh-agent v0.3.3 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect - github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/xeipuuv/gojsonschema v1.2.0 // indirect - github.com/xen0n/gosmopolitan v1.3.0 // indirect - github.com/xhit/go-str2duration/v2 v2.1.0 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/yagipy/maintidx v1.0.0 // indirect - github.com/yeya24/promlinter v0.3.0 // indirect - github.com/ykadowak/zerologlint v0.1.5 // indirect - github.com/yuin/goldmark v1.7.13 // indirect - github.com/yuin/goldmark-emoji v1.0.6 // indirect - gitlab.com/bosi/decorder v0.4.2 // indirect - go-simpler.org/musttag v0.14.0 // indirect - go-simpler.org/sloglint v0.12.0 // indirect - go.augendre.info/arangolint v0.4.0 // indirect - go.augendre.info/fatcontext v0.9.0 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect - go.opentelemetry.io/otel/metric v1.46.0 // indirect - go.opentelemetry.io/otel/sdk v1.44.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect - go.opentelemetry.io/otel/trace v1.46.0 // indirect - go.opentelemetry.io/proto/otlp v1.10.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect - go.yaml.in/yaml/v2 v2.4.3 // indirect - go.yaml.in/yaml/v3 v3.0.5 // indirect - golang.org/x/crypto v0.55.0 // indirect - golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect - golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/text v0.41.0 // indirect - golang.org/x/tools v0.49.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/protobuf v1.36.11 // indirect - gopkg.in/warnings.v0 v0.1.2 // indirect - gotest.tools/gotestsum v1.12.2 // indirect - honnef.co/go/tools v0.7.0 // indirect - mvdan.cc/gofumpt v0.9.2 // indirect - mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect -) - -replace github.com/supabase/cli/pkg v1.0.0 => ./pkg - -replace github.com/fsnotify/fsevents v0.2.0 => ./fsevents - -tool ( - github.com/golangci/golangci-lint/v2/cmd/golangci-lint - github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen - gotest.tools/gotestsum -) diff --git a/apps/cli-go/go.sum b/apps/cli-go/go.sum deleted file mode 100644 index 9cf1bac004..0000000000 --- a/apps/cli-go/go.sum +++ /dev/null @@ -1,1232 +0,0 @@ -4d63.com/gocheckcompilerdirectives v1.3.0 h1:Ew5y5CtcAAQeTVKUVFrE7EwHMrTO6BggtEj8BZSjZ3A= -4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY= -4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU= -4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0= -charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU= -charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA= -codeberg.org/chavacava/garif v0.2.0 h1:F0tVjhYbuOCnvNcU3YSpO6b3Waw6Bimy4K0mM8y6MfY= -codeberg.org/chavacava/garif v0.2.0/go.mod h1:P2BPbVbT4QcvLZrORc2T29szK3xEOlnl0GiPTJmEqBQ= -codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh2an7YmodI= -codeberg.org/polyfloyd/go-errorlint v1.9.0/go.mod h1:GPRRu2LzVijNn4YkrZYJfatQIdS+TrcK8rL5Xs24qw8= -dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= -dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y= -dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI= -dev.gaijin.team/go/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo= -dev.gaijin.team/go/golib v0.6.0/go.mod h1:uY1mShx8Z/aNHWDyAkZTkX+uCi5PdX7KsG1eDQa2AVE= -github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8= -github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c= -github.com/Abirdcfly/dupword v0.1.7 h1:2j8sInznrje4I0CMisSL6ipEBkeJUJAmK1/lfoNGWrQ= -github.com/Abirdcfly/dupword v0.1.7/go.mod h1:K0DkBeOebJ4VyOICFdppB23Q0YMOgVafM0zYW0n9lF4= -github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo= -github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY= -github.com/AlwxSin/noinlineerr v1.0.5 h1:RUjt63wk1AYWTXtVXbSqemlbVTb23JOSRiNsshj7TbY= -github.com/AlwxSin/noinlineerr v1.0.5/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc= -github.com/Antonboom/errname v1.1.1 h1:bllB7mlIbTVzO9jmSWVWLjxTEbGBVQ1Ff/ClQgtPw9Q= -github.com/Antonboom/errname v1.1.1/go.mod h1:gjhe24xoxXp0ScLtHzjiXp0Exi1RFLKJb0bVBtWKCWQ= -github.com/Antonboom/nilnil v1.1.1 h1:9Mdr6BYd8WHCDngQnNVV0b554xyisFioEKi30sksufQ= -github.com/Antonboom/nilnil v1.1.1/go.mod h1:yCyAmSw3doopbOWhJlVci+HuyNRuHJKIv6V2oYQa8II= -github.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ= -github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= -github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/ClickHouse/clickhouse-go-linter v1.2.0 h1:zbm174up3hTKjp0wKZVnTzRiG7tSF5XZF0FJG/MuCBI= -github.com/ClickHouse/clickhouse-go-linter v1.2.0/go.mod h1:pLorS7ffPTfuUV9M0SJgfHA/h/WQPQUk2FWG9x74cQ4= -github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g= -github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= -github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/MirrexOne/unqueryvet v1.5.4 h1:38QOxShO7JmMWT+eCdDMbcUgGCOeJphVkzzRgyLJgsQ= -github.com/MirrexOne/unqueryvet v1.5.4/go.mod h1:fs9Zq6eh1LRIhsDIsxf9PONVUjYdFHdtkHIgZdJnyPU= -github.com/Netflix/go-env v0.1.2 h1:0DRoLR9lECQ9Zqvkswuebm3jJ/2enaDX6Ei8/Z+EnK0= -github.com/Netflix/go-env v0.1.2/go.mod h1:WlIhYi++8FlKNJtrop1mjXYAJMzv1f43K4MqCoh0yGE= -github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4= -github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo= -github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= -github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= -github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= -github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= -github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM= -github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI= -github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU= -github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E= -github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= -github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ= -github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q= -github.com/alexkohler/prealloc v1.1.0 h1:cKGRBqlXw5iyQGLYhrXrDlcHxugXpTq4tQ5c91wkf8M= -github.com/alexkohler/prealloc v1.1.0/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig= -github.com/alfatraining/structtag v1.0.0 h1:2qmcUqNcCoyVJ0up879K614L9PazjBSFruTB0GOFjCc= -github.com/alfatraining/structtag v1.0.0/go.mod h1:p3Xi5SwzTi+Ryj64DqjLWz7XurHxbGsq6y3ubePJPus= -github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= -github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= -github.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w= -github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg= -github.com/andybalholm/brotli v1.2.3 h1:8H1qwOkl2LPfjf3YezB90JnCliZb6SInJ/OJkEbA5NQ= -github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= -github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/ashanbrown/forbidigo/v2 v2.3.1 h1:KAZijvQ7zeIBKbhikT4jCm0TLYXC4u78bTiLh/8JROI= -github.com/ashanbrown/forbidigo/v2 v2.3.1/go.mod h1:2QDkLTzU6TV937eFROamXrW92M3paehdae4HCDCOZCM= -github.com/ashanbrown/makezero/v2 v2.2.1 h1:A7uU8dgB1PA9aelTxHMfHIQ8Qev8AB3JLxJUBUsejqM= -github.com/ashanbrown/makezero/v2 v2.2.1/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY= -github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= -github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= -github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= -github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= -github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/beorn7/perks v0.0.0-20150223135152-b965b613227f/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bitfield/gotestdox v0.2.2 h1:x6RcPAbBbErKLnapz1QeAlf3ospg8efBsedU93CDsnE= -github.com/bitfield/gotestdox v0.2.2/go.mod h1:D+gwtS0urjBrzguAkTM2wodsTQYFHdpx8eqRJ3N+9pY= -github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw= -github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w= -github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo= -github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= -github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= -github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/bombsimon/wsl/v4 v4.7.0 h1:1Ilm9JBPRczjyUs6hvOPKvd7VL1Q++PL8M0SXBDf+jQ= -github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg= -github.com/bombsimon/wsl/v5 v5.8.0 h1:JTkyfs4yl8SPejrCF2GdABXE+mO1WvM7iUYzRWlsxDs= -github.com/bombsimon/wsl/v5 v5.8.0/go.mod h1:AbOLsulgkqP4ZnitHf9gwPtCOGlrzkk0jb0uNxRSY0o= -github.com/breml/bidichk v0.3.3 h1:WSM67ztRusf1sMoqH6/c4OBCUlRVTKq+CbSeo0R17sE= -github.com/breml/bidichk v0.3.3/go.mod h1:ISbsut8OnjB367j5NseXEGGgO/th206dVa427kR8YTE= -github.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDwg= -github.com/breml/errchkjson v0.4.1/go.mod h1:a23OvR6Qvcl7DG/Z4o0el6BRAjKnaReoPQFciAl9U3s= -github.com/bugsnag/bugsnag-go v1.0.5-0.20150529004307-13fd6b8acda0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= -github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= -github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/butuzov/ireturn v0.4.1 h1:vWb3NO4t77iku/sjCQ/2pHTQeOmxEhjIriJqRLg1Y+I= -github.com/butuzov/ireturn v0.4.1/go.mod h1:q+DXKzTDV5guNuXLnIab9fKXizTn2miZHLhxH7V/GB4= -github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc= -github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI= -github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ= -github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc= -github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc= -github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= -github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charithe/durationcheck v0.0.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1DidBcABctk= -github.com/charithe/durationcheck v0.0.11/go.mod h1:x5iZaixRNl8ctbM+3B2RrPG5t856TxRyVQEnbIEM2X4= -github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= -github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= -github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= -github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= -github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= -github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= -github.com/charmbracelet/glamour v1.0.0 h1:AWMLOVFHTsysl4WV8T8QgkQ0s/ZNZo7CiE4WKhk8l08= -github.com/charmbracelet/glamour v1.0.0/go.mod h1:DSdohgOBkMr2ZQNhw4LZxSGpx3SvpeujNoXrQyH2hxo= -github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= -github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= -github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= -github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= -github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 h1:OqDqxQZliC7C8adA7KjelW3OjtAxREfeHkNcd66wpeI= -github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318/go.mod h1:Y6kE2GzHfkyQQVCSL9r2hwokSrIlHGzZG+71+wDYSZI= -github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= -github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= -github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= -github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= -github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= -github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= -github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= -github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= -github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= -github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= -github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= -github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= -github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= -github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs= -github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk= -github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= -github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= -github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= -github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= -github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= -github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/compose-spec/compose-go/v2 v2.9.1 h1:8UwI+ujNU+9Ffkf/YgAm/qM9/eU7Jn8nHzWG721W4rs= -github.com/compose-spec/compose-go/v2 v2.9.1/go.mod h1:Oky9AZGTRB4E+0VbTPZTUu4Kp+oEMMuwZXZtPPVT1iE= -github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= -github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= -github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= -github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containers/common v0.64.2 h1:1xepE7QwQggUXxmyQ1Dbh6Cn0yd7ktk14sN3McSWf5I= -github.com/containers/common v0.64.2/go.mod h1:o29GfYy4tefUuShm8mOn2AiL5Mpzdio+viHI7n24KJ4= -github.com/containers/storage v1.59.1 h1:11Zu68MXsEQGBBd+GadPrHPpWeqjKS8hJDGiAHgIqDs= -github.com/containers/storage v1.59.1/go.mod h1:KoAYHnAjP3/cTsRS+mmWZGkufSY2GACiKQ4V3ZLQnR0= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= -github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= -github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= -github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= -github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= -github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= -github.com/daixiang0/gci v0.13.7 h1:+0bG5eK9vlI08J+J/NWGbWPTNiXPG4WhNLJOkSxWITQ= -github.com/daixiang0/gci v0.13.7/go.mod h1:812WVN6JLFY9S6Tv76twqmNqevN0pa3SX3nih0brVzQ= -github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= -github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= -github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY= -github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= -github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= -github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= -github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= -github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= -github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= -github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= -github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dnephin/pflag v1.0.7 h1:oxONGlWxhmUct0YzKTgrpQv9AUA1wtPBn7zuSjJqptk= -github.com/dnephin/pflag v1.0.7/go.mod h1:uxE91IoWURlOiTUIA8Mq5ZZkAv3dPUfZNaT80Zm7OQE= -github.com/docker/cli v28.5.2+incompatible h1:XmG99IHcBmIAoC1PPg9eLBZPlTrNUAijsHLm8PjhBlg= -github.com/docker/cli v28.5.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= -github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= -github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY= -github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= -github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= -github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M= -github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= -github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= -github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= -github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= -github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= -github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= -github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= -github.com/ecies/go/v2 v2.0.11 h1:xYhtMdLiqNi02oLirFmLyNbVXw6250h3WM6zJryQdiM= -github.com/ecies/go/v2 v2.0.11/go.mod h1:LPRzoefP0Tam+1uesQOq3Gtb6M2OwlFUnXBTtBAKfDQ= -github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= -github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= -github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= -github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= -github.com/ethereum/go-ethereum v1.17.0 h1:2D+1Fe23CwZ5tQoAS5DfwKFNI1HGcTwi65/kRlAVxes= -github.com/ethereum/go-ethereum v1.17.0/go.mod h1:2W3msvdosS/MCWytpqTcqgFiRYbTH59FxDJzqah120o= -github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= -github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= -github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= -github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= -github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= -github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/firefart/nonamedreturns v1.0.6 h1:vmiBcKV/3EqKY3ZiPxCINmpS431OcE1S47AQUwhrg8E= -github.com/firefart/nonamedreturns v1.0.6/go.mod h1:R8NisJnSIpvPWheCq0mNRXJok6D8h7fagJTF8EMEwCo= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= -github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= -github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= -github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= -github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= -github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= -github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI= -github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ= -github.com/getkin/kin-openapi v0.144.0 h1:hIRcTH+KjLfkLpYU6bSSfdFpi0fZi1fp+hSPi4aQu9Y= -github.com/getkin/kin-openapi v0.144.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY= -github.com/getsentry/sentry-go v0.49.0 h1:Ehejknu1l023Ub7QoRBVLAI7g3Jnhqku4oWx4B4Sh5s= -github.com/getsentry/sentry-go v0.49.0/go.mod h1:nuMJAoCfe1u0Bts2ocyNI+TW8HT84vRMqwA5Qq/SKUI= -github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0= -github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI= -github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= -github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= -github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog= -github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ= -github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= -github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= -github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY= -github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= -github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= -github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= -github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= -github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= -github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM= -github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.30.4 h1:9Rcod2ZPO6mOEG6b4GqyoHE/H6//Ze0RuhOo1hT1x0w= -github.com/go-playground/validator/v10 v10.30.4/go.mod h1:numpT+RPLE91R9oYWMY/R9zRgJBewr3IXHko4OISPpk= -github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= -github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= -github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= -github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= -github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s= -github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw= -github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4= -github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ= -github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw= -github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY= -github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco= -github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4= -github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA= -github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA= -github.com/go-toolsmith/pkgload v1.2.2 h1:0CtmHq/02QhxcF7E9N5LIFcYFsMR5rdovfqTtRKkgIk= -github.com/go-toolsmith/pkgload v1.2.2/go.mod h1:R2hxLNRKuAsiXCo2i5J6ZQPhnPMOVtU+f0arbFPWCus= -github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= -github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw= -github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= -github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= -github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= -github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= -github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY= -github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= -github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= -github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= -github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= -github.com/godoc-lint/godoc-lint v0.11.2 h1:Bp0FkJWoSdNsBikdNgIcgtaoo+xz6I/Y9s5WSBQUeeM= -github.com/godoc-lint/godoc-lint v0.11.2/go.mod h1:iVpGdL1JCikNH2gGeAn3Hh+AgN5Gx/I/cxV+91L41jo= -github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= -github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= -github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= -github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0= -github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ= -github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 h1:CbTB8KpqnViI6lIXxp03Oclc4VFHi3K4BWC1TacsZ+A= -github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E= -github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarogrvjO9AfiW3B4U= -github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss= -github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE= -github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY= -github.com/golangci/golangci-lint/v2 v2.12.2 h1:7+d1uY0bq1MU2UV3R5pW5Q7QWdcoq4naMRXM+gsJKrs= -github.com/golangci/golangci-lint/v2 v2.12.2/go.mod h1:opqHHuIcTG2R+4akzWMd4o1BnD9/1LcjICWOujr91U8= -github.com/golangci/golines v0.15.0 h1:Qnph25g8Y1c5fdo1X7GaRDGgnMHgnxh4Gk4VfPTtRx0= -github.com/golangci/golines v0.15.0/go.mod h1:AZjXd23tbHMpowhtnGlj9KCNsysj72aeZVVHnVcZx10= -github.com/golangci/misspell v0.8.0 h1:qvxQhiE2/5z+BVRo1kwYA8yGz+lOlu5Jfvtx2b04Jbg= -github.com/golangci/misspell v0.8.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg= -github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg= -github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw= -github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s= -github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= -github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba h1:lqtcnSMDuuJdu/LrKWi5RJzpSNLOJXYe/nzQutTI5kg= -github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba/go.mod h1:sCBNcpRmhJCtbFGz49+IM3ETTFf7QdJ30AeYCd43NKk= -github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2bRA5htgAG9r7s3tHsfjIhN98WshBTJ9jM= -github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s= -github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM= -github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc= -github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4= -github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= -github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= -github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= -github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs= -github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw= -github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= -github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= -github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= -github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= -github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= -github.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8= -github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc= -github.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk= -github.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY= -github.com/gostaticanalysis/nilerr v0.1.2 h1:S6nk8a9N8g062nsx63kUkF6AzbHGw7zzyHMcpu52xQU= -github.com/gostaticanalysis/nilerr v0.1.2/go.mod h1:A19UHhoY3y8ahoL7YKz6sdjDtduwTSI4CsymaC2htPA= -github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= -github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8= -github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= -github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= -github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= -github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= -github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo= -github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw= -github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= -github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA= -github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= -github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= -github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= -github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= -github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= -github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= -github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= -github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8= -github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= -github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= -github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jgautheron/goconst v1.10.0 h1:Ptt+OoE4NaEWKhLrWrrN3IpZdGLiqaf7WLnEX/iv4Jw= -github.com/jgautheron/goconst v1.10.0/go.mod h1:0p+wv1lFOiUr0IlNNT1nrm6+8DB8u2sU6KHGzFRXHDc= -github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= -github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8= -github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= -github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ= -github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY= -github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0= -github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY= -github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= -github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/kisielk/errcheck v1.10.0 h1:Lvs/YAHP24YKg08LA8oDw2z9fJVme090RAXd90S+rrw= -github.com/kisielk/errcheck v1.10.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE= -github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= -github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= -github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kulti/thelper v0.7.1 h1:fI8QITAoFVLx+y+vSyuLBP+rcVIB8jKooNSCT2EiI98= -github.com/kulti/thelper v0.7.1/go.mod h1:NsMjfQEy6sd+9Kfw8kCP61W1I0nerGSYSFnGaxQkcbs= -github.com/kunwardeep/paralleltest v1.0.15 h1:ZMk4Qt306tHIgKISHWFJAO1IDQJLc6uDyJMLyncOb6w= -github.com/kunwardeep/paralleltest v1.0.15/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4= -github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI= -github.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ= -github.com/ldez/exptostd v0.4.5/go.mod h1:QRjHRMXJrCTIm9WxVNH6VW7oN7KrGSht69bIRwvdFsM= -github.com/ldez/gomoddirectives v0.8.0 h1:JqIuTtgvFC2RdH1s357vrE23WJF2cpDCPFgA/TWDGpk= -github.com/ldez/gomoddirectives v0.8.0/go.mod h1:jutzamvZR4XYJLr0d5Honycp4Gy6GEg2mS9+2YX3F1Q= -github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o= -github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas= -github.com/ldez/structtags v0.6.1 h1:bUooFLbXx41tW8SvkfwfFkkjPYvFFs59AAMgVg6DUBk= -github.com/ldez/structtags v0.6.1/go.mod h1:YDxVSgDy/MON6ariaxLF2X09bh19qL7MtGBN5MrvbdY= -github.com/ldez/tagliatelle v0.7.2 h1:KuOlL70/fu9paxuxbeqlicJnCspCRjH0x8FW+NfgYUk= -github.com/ldez/tagliatelle v0.7.2/go.mod h1:PtGgm163ZplJfZMZ2sf5nhUT170rSuPgBimoyYtdaSI= -github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc= -github.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3iRrzvDQ= -github.com/leodido/go-urn v1.5.0 h1:pLqT2kq1zpHW/1D18QMjMpdtX7cekxqtJJjg5ANyWw0= -github.com/leodido/go-urn v1.5.0/go.mod h1:9BORnCDhdPBJNDEX+w1bJisa8yOKYi116VeO96s4ifE= -github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= -github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= -github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= -github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= -github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= -github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE= -github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U= -github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww= -github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM= -github.com/manuelarte/funcorder v0.6.0 h1:0hBngc4fa1IgNiI65A7sFGkMvoMCc878RjqB5V7rWP0= -github.com/manuelarte/funcorder v0.6.0/go.mod h1:id3NDhXdQBmeqXH7eVC6Z89xS6JxvZ8kF9xUxpArU/g= -github.com/maratori/testableexamples v1.0.1 h1:HfOQXs+XgfeRBJ+Wz0XfH+FHnoY9TVqL6Fcevpzy4q8= -github.com/maratori/testableexamples v1.0.1/go.mod h1:XE2F/nQs7B9N08JgyRmdGjYVGqxWwClLPCGSQhXQSrQ= -github.com/maratori/testpackage v1.1.2 h1:ffDSh+AgqluCLMXhM19f/cpvQAKygKAJXFl9aUjmbqs= -github.com/maratori/testpackage v1.1.2/go.mod h1:8F24GdVDFW5Ew43Et02jamrVMNXLUNaOynhDssITGfc= -github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4= -github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs= -github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= -github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= -github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= -github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= -github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mgechev/revive v1.15.0 h1:vJ0HzSBzfNyPbHKolgiFjHxLek9KUijhqh42yGoqZ8Q= -github.com/mgechev/revive v1.15.0/go.mod h1:LlAKO3QQe9OJ0pVZzI2GPa8CbXGZ/9lNpCGvK4T/a8A= -github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= -github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= -github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= -github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= -github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= -github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= -github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= -github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= -github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= -github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= -github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= -github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= -github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= -github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= -github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= -github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= -github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= -github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg= -github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= -github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= -github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= -github.com/nunnatsa/ginkgolinter v0.23.0 h1:x3o4DGYOWbBMP/VdNQKgSj+25aJKx2Pe6lHr8gBcgf8= -github.com/nunnatsa/ginkgolinter v0.23.0/go.mod h1:9qN1+0akwXEccwV1CAcCDfcoBlWXHB+ML9884pL4SZ4= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oapi-codegen/nullable v1.2.0 h1:VflFkDW980KhBPiFF7nWSyjg+r4Obqj8lXipV0UkP5w= -github.com/oapi-codegen/nullable v1.2.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= -github.com/oapi-codegen/oapi-codegen/v2 v2.7.1 h1:a7Ab7YlpqkVG5HKrTaeFstm32Z5QOnyjnbsCO0jiMYM= -github.com/oapi-codegen/oapi-codegen/v2 v2.7.1/go.mod h1:qzFy6iuobJw/hD1aRILee4G87/ShmhR0xYCwcUtZMCw= -github.com/oapi-codegen/runtime v1.7.0 h1:t7358VYPvNbWJ9gdAkIK/smVeHpBf6yp8VTsaZsb/7k= -github.com/oapi-codegen/runtime v1.7.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= -github.com/oasdiff/yaml v0.1.1 h1:6nHx+pn9gBRM6YpBlFZFQGCCd1nuvqOBtTD3KKTgGxY= -github.com/oasdiff/yaml v0.1.1/go.mod h1:EYJNoyktvWMJ0Hmhx+6qTaqMOsalUaRGT8Sj1hNcegU= -github.com/oasdiff/yaml3 v0.0.14 h1:aLJee3hxBK2H5wdXd9iPcIXb93Nty1Ge0pT171eHtkw= -github.com/oasdiff/yaml3 v0.0.14/go.mod h1:csto2xfDjYccdUn/yw/bPjj/cYTdp6HtFA0J4TWG+gg= -github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= -github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= -github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= -github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= -github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA= -github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88= -github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= -github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.28.2 h1:DTrMfpqxiNUyQ3Y0zhn1n3cOO2euFgQPYIpkWwxVFps= -github.com/onsi/ginkgo/v2 v2.28.2/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= -github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= -github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= -github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= -github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= -github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= -github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= -github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= -github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= -github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= -github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= -github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= -github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= -github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= -github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= -github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= -github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posthog/posthog-go v1.25.1 h1:jo5cW5GGoY41noy33Jn6A2N9Z5yrIGKHBr4DjAvnCrc= -github.com/posthog/posthog-go v1.25.1/go.mod h1:rN0C9Urenhytgo3KQ/QVorGkyGfpouquDTSs2Ujzr+g= -github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= -github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= -github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= -github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA= -github.com/quasilyte/go-ruleguard v0.4.5/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE= -github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY= -github.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= -github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= -github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng= -github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU= -github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= -github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= -github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= -github.com/raeperd/recvcheck v0.2.0 h1:GnU+NsbiCqdC2XX5+vMZzP+jAJC5fht7rcVTAhX74UI= -github.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryancurrah/gomodguard v1.4.1 h1:eWC8eUMNZ/wM/PWuZBv7JxxqT5fiIKSIyTvjb7Elr+g= -github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I= -github.com/ryancurrah/gomodguard/v2 v2.1.3 h1:E7sz3PJwE9Ba1reVxSpF6XLCPJZ74Kfw/LabTNM4GIA= -github.com/ryancurrah/gomodguard/v2 v2.1.3/go.mod h1:CQicdLGatWMxLX53JzoBjYlsNZhHbmLv2AVa0s2aivU= -github.com/ryanrolds/sqlclosecheck v0.6.0 h1:pEyL9okISdg1F1SEpJNlrEotkTGerv5BMk7U4AG0eVg= -github.com/ryanrolds/sqlclosecheck v0.6.0/go.mod h1:xyX16hsDaCMXHrMJ3JMzGf5OpDfHTOTTQrT7HOFUmeU= -github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= -github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= -github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= -github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0= -github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= -github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= -github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= -github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ= -github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/securego/gosec/v2 v2.26.1 h1:gdkttGhQFVehqRJ8grKH4DrpqM/QlPKNHBnl8QgcEC4= -github.com/securego/gosec/v2 v2.26.1/go.mod h1:57UW4p0uoP3kxoTkhoo3axLdVAi+OWrLg/Ax/kdqtPE= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= -github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= -github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= -github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= -github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= -github.com/sonatard/noctx v0.5.1 h1:wklWg9c9ZYugOAk7qG4yP4PBrlQsmSLPTvW1K4PRQMs= -github.com/sonatard/noctx v0.5.1/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas= -github.com/sourcegraph/go-diff v0.8.0 h1:ipIyu4cTsLbIrln4l0qtHA3r0a7gyK4ntKjtQytHhvY= -github.com/sourcegraph/go-diff v0.8.0/go.mod h1:hWlcO7Al+UZStZAP8rBumHpCK5ZHQ5BXsMls8p4+F5E= -github.com/speakeasy-api/jsonpath v0.6.3 h1:c+QPwzAOdrWvzycuc9HFsIZcxKIaWcNpC+xhOW9rJxU= -github.com/speakeasy-api/jsonpath v0.6.3/go.mod h1:2cXloNuQ+RSXi5HTRaeBh7JEmjRXTiaKpFTdZiL7URI= -github.com/speakeasy-api/openapi v1.19.2 h1:md90tE71/M8jS3cuRlsuWP5Aed4xoG5PSRvXeZgCv/M= -github.com/speakeasy-api/openapi v1.19.2/go.mod h1:UfKa7FqE4jgexJZuj51MmdHAFGmDv0Zaw3+yOd81YKU= -github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= -github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= -github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= -github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= -github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= -github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= -github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= -github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= -github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= -github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g= -github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= -github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= -github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= -github.com/stripe/pg-schema-diff v1.0.9 h1:oI1MPWAEuvuc/TCeGoJJ6sc+70Rs+epFM3LXvTFZxYE= -github.com/stripe/pg-schema-diff v1.0.9/go.mod h1:3IctPaAqm+0LtWw/GiwyRoRlU1/N/+00+eXVk0KZIHs= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA= -github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= -github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= -github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= -github.com/tetafro/godot v1.5.6 h1:IEkrFCwXaYHlOn4mGzGS3F3dkP6m9t0jpwqBFPIkKiA= -github.com/tetafro/godot v1.5.6/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU= -github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c= -github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw= -github.com/tidwall/jsonc v0.3.3 h1:RVQqL3xFfDkKKXIDsrBiVQiEpBtxoKbmMXONb2H/y2w= -github.com/tidwall/jsonc v0.3.3/go.mod h1:dw+3CIxqHi+t8eFSpzzMlcVYxKp08UP5CD8/uSFCyJE= -github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4 h1:SiHe5XLTn9sFWJ5pBwJ5FN/4j34q9ZlOAD//kMoMYp0= -github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4/go.mod h1:sDHLK7rb/59v/ZxZ7KtymgcoxuUMxjXq8gtu9VMOK8M= -github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M= -github.com/timonwong/loggercheck v0.11.0/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8= -github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is= -github.com/tomarrell/wrapcheck/v2 v2.12.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo= -github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= -github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= -github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI= -github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA= -github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g= -github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= -github.com/uudashr/gocognit v1.2.1 h1:CSJynt5txTnORn/DkhiB4mZjwPuifyASC8/6Q0I/QS4= -github.com/uudashr/gocognit v1.2.1/go.mod h1:acaubQc6xYlXFEMb9nWX2dYBzJ/bIjEkc1zzvyIZg5Q= -github.com/uudashr/iface v1.4.2 h1:06Vq5RKVYThBsj0Bnw4oasMjD1r+7CE/bcKOA8dVSvg= -github.com/uudashr/iface v1.4.2/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg= -github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= -github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= -github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= -github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM= -github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4= -github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= -github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= -github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= -github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= -github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs= -github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4= -github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw= -github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= -github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= -github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= -github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= -github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs= -github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo= -gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8= -go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ= -go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28= -go-simpler.org/musttag v0.14.0 h1:XGySZATqQYSEV3/YTy+iX+aofbZZllJaqwFWs+RTtSo= -go-simpler.org/musttag v0.14.0/go.mod h1:uP8EymctQjJ4Z1kUnjX0u2l60WfUdQxCwSNKzE1JEOE= -go-simpler.org/sloglint v0.12.0 h1:UzWDlLWNE5FLqsvyq3tWYHuQMbqrervOhT8qPl4Mmw4= -go-simpler.org/sloglint v0.12.0/go.mod h1:jBjjC2bm8rYrs88oTRlFX497kWjJsyZWYoNaXkGRI6I= -go.augendre.info/arangolint v0.4.0 h1:xSCZjRoS93nXazBSg5d0OGCi9APPLNMmmLrC995tR50= -go.augendre.info/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA= -go.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDojE= -go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= -go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= -go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf8jF6WbuGQWUVcqgyWtTR0kOOAWY1DYZ+UhvdmQPw= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= -go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8= -go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o= -go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA= -go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk= -go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= -go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= -go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= -go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= -go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c= -go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI= -go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= -go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= -go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= -go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= -golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= -golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= -golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= -golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 h1:qWFG1Dj7TBjOjOvhEOkmyGPVoquqUKnIU0lEVLp8xyk= -golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= -golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= -golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98= -golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= -golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= -golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= -golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= -golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= -golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= -golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= -golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= -gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= -google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= -gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/gotestsum v1.12.2 h1:eli4tu9Q2D/ogDsEGSr8XfQfl7mT0JsGOG6DFtUiZ/Q= -gotest.tools/gotestsum v1.12.2/go.mod h1:kjRtCglPZVsSU0hFHX3M5VWBM6Y63emHuB14ER1/sow= -gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= -gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU= -honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc= -mvdan.cc/gofumpt v0.9.2 h1:zsEMWL8SVKGHNztrx6uZrXdp7AX8r421Vvp23sz7ik4= -mvdan.cc/gofumpt v0.9.2/go.mod h1:iB7Hn+ai8lPvofHd9ZFGVg2GOr8sBUw1QUWjNbmIL/s= -mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 h1:ssMzja7PDPJV8FStj7hq9IKiuiKhgz9ErWw+m68e7DI= -mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15/go.mod h1:4M5MMXl2kW6fivUT6yRGpLLPNfuGtU2Z0cPvFquGDYU= diff --git a/apps/cli-go/internal/db/diff/diff.go b/apps/cli-go/internal/db/diff/diff.go deleted file mode 100644 index f4c05fb801..0000000000 --- a/apps/cli-go/internal/db/diff/diff.go +++ /dev/null @@ -1,254 +0,0 @@ -package diff - -import ( - "context" - "fmt" - "io" - "io/fs" - "os" - "path/filepath" - "regexp" - "sort" - "strconv" - "strings" - "time" - - "github.com/cenkalti/backoff/v4" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/network" - "github.com/docker/go-connections/nat" - "github.com/go-errors/errors" - "github.com/jackc/pgconn" - "github.com/jackc/pgx/v4" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/db/start" - "github.com/supabase/cli/internal/migration/new" - "github.com/supabase/cli/internal/utils" - configpkg "github.com/supabase/cli/pkg/config" - "github.com/supabase/cli/pkg/migration" - "github.com/supabase/cli/pkg/parser" -) - -type DiffFunc func(context.Context, pgconn.Config, pgconn.Config, []string, ...func(*pgx.ConnConfig)) (string, error) - -// DatabaseDiff is the result of diffing a target database against a shadow baseline. -type DatabaseDiff struct { - SQL string -} - -func Run(ctx context.Context, schema []string, file string, config pgconn.Config, differ DiffFunc, fsys afero.Fs, options ...func(*pgx.ConnConfig)) (err error) { - result, err := DiffDatabase(ctx, schema, config, os.Stderr, fsys, differ, options...) - if err != nil { - return err - } - out := result.SQL - branch := utils.GetGitBranch(fsys) - fmt.Fprintln(os.Stderr, "Finished "+utils.Aqua("supabase db diff")+" on branch "+utils.Aqua(branch)+".\n") - if err := SaveDiff(result, file, fsys); err != nil { - return err - } - drops := findDropStatements(out) - if len(drops) > 0 { - fmt.Fprintln(os.Stderr, "Found drop statements in schema diff. Please double check if these are expected:") - fmt.Fprintln(os.Stderr, utils.Yellow(strings.Join(drops, "\n"))) - } - return nil -} - -func loadDeclaredSchemas(fsys afero.Fs) ([]string, error) { - if schemas := utils.Config.Db.Migrations.SchemaPaths; len(schemas) > 0 { - return schemas.SQLFiles( - afero.NewIOFS(fsys), - configpkg.WithSkipEmptyGlobs(), - configpkg.WithErrorOnAllSkippedGlobs(), - ) - } - // When pg-delta is enabled, declarative path is the source of truth (config or default). - if utils.IsPgDeltaEnabled() { - declDir := utils.GetDeclarativeDir() - if exists, err := afero.DirExists(fsys, declDir); err == nil && exists { - var declared []string - if err := afero.Walk(fsys, declDir, func(path string, info fs.FileInfo, err error) error { - if err != nil { - return err - } - if info.Mode().IsRegular() && filepath.Ext(info.Name()) == ".sql" { - declared = append(declared, path) - } - return nil - }); err != nil { - return nil, errors.Errorf("failed to walk declarative dir: %w", err) - } - sort.Strings(declared) - return declared, nil - } - } - if exists, err := afero.DirExists(fsys, utils.SchemasDir); err != nil { - return nil, errors.Errorf("failed to check schemas: %w", err) - } else if !exists { - return nil, nil - } - var declared []string - if err := afero.Walk(fsys, utils.SchemasDir, func(path string, info fs.FileInfo, err error) error { - if err != nil { - return err - } - if info.Mode().IsRegular() && filepath.Ext(info.Name()) == ".sql" { - declared = append(declared, path) - } - return nil - }); err != nil { - return nil, errors.Errorf("failed to walk dir: %w", err) - } - // Keep file application order deterministic so diff output stays stable across - // filesystems and operating systems. This is only if no schema paths in config are set. - sort.Strings(declared) - return declared, nil -} - -var warnDiff = `WARNING: The diff tool is not foolproof, so you may need to manually rearrange and modify the generated migration. -Run ` + utils.Aqua("supabase db reset") + ` to verify that the new migration does not generate errors.` - -func SaveDiff(result DatabaseDiff, file string, fsys afero.Fs) error { - out := result.SQL - if len(out) < 2 { - fmt.Fprintln(os.Stderr, "No schema changes found") - } else if len(file) > 0 { - path := new.GetMigrationPath(utils.GetCurrentTimestamp(), file) - if err := utils.WriteFile(path, []byte(out), fsys); err != nil { - return err - } - fmt.Fprintln(os.Stderr, warnDiff) - } else { - fmt.Println(out) - } - return nil -} - -// https://github.com/djrobstep/migra/blob/master/migra/statements.py#L6 -var dropStatementPattern = regexp.MustCompile(`(?i)drop\s+`) - -func findDropStatements(out string) []string { - lines, err := parser.SplitAndTrim(strings.NewReader(out)) - if err != nil { - return nil - } - var drops []string - for _, line := range lines { - if dropStatementPattern.MatchString(line) { - drops = append(drops, line) - } - } - return drops -} - -func CreateShadowDatabase(ctx context.Context, port uint16) (string, error) { - // Disable background workers in shadow database - config := start.NewContainerConfig("-c", "max_worker_processes=0") - hostPort := strconv.FormatUint(uint64(port), 10) - hostConfig := container.HostConfig{ - PortBindings: nat.PortMap{"5432/tcp": []nat.PortBinding{{HostPort: hostPort}}}, - AutoRemove: true, - } - networkingConfig := network.NetworkingConfig{} - if utils.Config.Db.MajorVersion <= 14 { - hostConfig.Tmpfs = map[string]string{"/docker-entrypoint-initdb.d": ""} - } - return utils.DockerStart(ctx, config, hostConfig, networkingConfig, "") -} - -func ConnectShadowDatabase(ctx context.Context, timeout time.Duration, options ...func(*pgx.ConnConfig)) (conn *pgx.Conn, err error) { - // Retry until connected, cancelled, or timeout - policy := start.NewBackoffPolicy(ctx, timeout) - config := pgconn.Config{Port: utils.Config.Db.ShadowPort} - connect := func() (*pgx.Conn, error) { - return utils.ConnectLocalPostgres(ctx, config, options...) - } - return backoff.RetryWithData(connect, policy) -} - -// Required to bypass pg_cron check: https://github.com/citusdata/pg_cron/blob/main/pg_cron.sql#L3 -const CREATE_TEMPLATE = "CREATE DATABASE contrib_regression TEMPLATE postgres" - -// setupShadowConn applies the Supabase platform schema (auth, storage, realtime, -// etc.) to an already-connected shadow database and creates the pg_cron template -// database. It deliberately stops short of applying user migrations so that -// callers which only need the platform baseline (declarative apply) share the -// exact same starting point as callers that also replay migrations. -func setupShadowConn(ctx context.Context, conn *pgx.Conn, container string, fsys afero.Fs) error { - if err := start.SetupDatabase(ctx, conn, container[:12], os.Stderr, fsys); err != nil { - return err - } - if _, err := conn.Exec(ctx, CREATE_TEMPLATE); err != nil { - return errors.Errorf("failed to create template database: %w", err) - } - return nil -} - -// SetupShadowDatabase provisions the Supabase platform baseline (service schemas -// such as auth/storage/realtime) on a freshly created shadow database, without -// applying user migrations. Declarative apply uses this so the shadow matches the -// real database closely enough for Supabase-managed dependencies (auth.sessions, -// auth.jwt(), ...) to resolve. -func SetupShadowDatabase(ctx context.Context, container string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { - conn, err := ConnectShadowDatabase(ctx, 10*time.Second, options...) - if err != nil { - return err - } - defer conn.Close(context.Background()) - return setupShadowConn(ctx, conn, container, fsys) -} - -func MigrateShadowDatabase(ctx context.Context, container string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { - migrations, err := migration.ListLocalMigrations(utils.MigrationsDir, afero.NewIOFS(fsys)) - if err != nil { - return err - } - conn, err := ConnectShadowDatabase(ctx, 10*time.Second, options...) - if err != nil { - return err - } - defer conn.Close(context.Background()) - if err := setupShadowConn(ctx, conn, container, fsys); err != nil { - return err - } - return migration.ApplyMigrations(ctx, migrations, conn, afero.NewIOFS(fsys)) -} - -func DiffDatabase(ctx context.Context, schema []string, config pgconn.Config, w io.Writer, fsys afero.Fs, differ DiffFunc, options ...func(*pgx.ConnConfig)) (DatabaseDiff, error) { - fmt.Fprintln(w, "Creating shadow database...") - shadowSource, err := PrepareShadowSource(ctx, utils.IsLocalDatabase(config), fsys, options...) - if err != nil { - return DatabaseDiff{}, err - } - defer utils.DockerRemove(shadowSource.Container) - shadowConfig := shadowSource.Source - if shadowSource.TargetOverride != nil { - config = *shadowSource.TargetOverride - } - if len(schema) > 0 { - fmt.Fprintln(w, "Diffing schemas:", strings.Join(schema, ",")) - } else { - fmt.Fprintln(w, "Diffing schemas...") - } - output, err := differ(ctx, shadowConfig, config, schema, options...) - if err != nil { - return DatabaseDiff{}, err - } - return DatabaseDiff{SQL: output}, nil -} - -func migrateBaseDatabase(ctx context.Context, config pgconn.Config, migrations []string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { - fmt.Fprintln(os.Stderr, "Creating local database from declarative schemas:") - msg := make([]string, len(migrations)) - for i, m := range migrations { - msg[i] = fmt.Sprintf(" • %s", utils.Bold(m)) - } - fmt.Fprintln(os.Stderr, strings.Join(msg, "\n")) - conn, err := utils.ConnectLocalPostgres(ctx, config, options...) - if err != nil { - return err - } - defer conn.Close(context.Background()) - return migration.SeedGlobals(ctx, migrations, conn, afero.NewIOFS(fsys)) -} diff --git a/apps/cli-go/internal/db/diff/diff_test.go b/apps/cli-go/internal/db/diff/diff_test.go deleted file mode 100644 index af0cacf610..0000000000 --- a/apps/cli-go/internal/db/diff/diff_test.go +++ /dev/null @@ -1,516 +0,0 @@ -package diff - -import ( - "context" - "errors" - "io" - "net/http" - "os" - "path/filepath" - "testing" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/h2non/gock" - "github.com/jackc/pgconn" - "github.com/jackc/pgerrcode" - "github.com/jackc/pgx/v4" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/testing/apitest" - "github.com/supabase/cli/internal/testing/fstest" - "github.com/supabase/cli/internal/testing/helper" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/internal/utils/flags" - pkgconfig "github.com/supabase/cli/pkg/config" - "github.com/supabase/cli/pkg/migration" - "github.com/supabase/cli/pkg/pgtest" -) - -var dbConfig = pgconn.Config{ - Host: "db.supabase.co", - Port: 5432, - User: "admin", - Password: "password", - Database: "postgres", -} - -func TestLoadDeclaredSchemas(t *testing.T) { - t.Run("respects schema_paths order when pg-delta declarative dir exists", func(t *testing.T) { - originalConfig := utils.Config - t.Cleanup(func() { utils.Config = originalConfig }) - utils.Config.Db.Migrations.SchemaPaths = pkgconfig.Glob{ - "supabase/schemas/z_function.sql", - "supabase/schemas/a_table.sql", - } - utils.Config.Experimental.PgDelta = &pkgconfig.PgDeltaConfig{ - Enabled: true, - DeclarativeSchemaPath: utils.SchemasDir, - } - fsys := afero.NewMemMapFs() - require.NoError(t, fsys.MkdirAll(utils.SchemasDir, 0755)) - require.NoError(t, afero.WriteFile(fsys, "supabase/schemas/a_table.sql", []byte("create table a();"), 0644)) - require.NoError(t, afero.WriteFile(fsys, "supabase/schemas/z_function.sql", []byte("create function z() returns void language sql as $$ select 1 $$;"), 0644)) - - declared, err := loadDeclaredSchemas(fsys) - - require.NoError(t, err) - assert.Equal(t, []string{ - "supabase/schemas/z_function.sql", - "supabase/schemas/a_table.sql", - }, declared) - }) - - t.Run("expands schema_paths directory entries deterministically", func(t *testing.T) { - originalConfig := utils.Config - t.Cleanup(func() { utils.Config = originalConfig }) - utils.Config.Db.Migrations.SchemaPaths = pkgconfig.Glob{utils.DeclarativeDir} - fsys := afero.NewMemMapFs() - require.NoError(t, fsys.MkdirAll(filepath.Join(utils.DeclarativeDir, "nested"), 0755)) - require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.DeclarativeDir, "nested", "b.sql"), []byte("select 2;"), 0644)) - require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.DeclarativeDir, "a.sql"), []byte("select 1;"), 0644)) - - declared, err := loadDeclaredSchemas(fsys) - - require.NoError(t, err) - assert.Equal(t, []string{ - filepath.Join(utils.DeclarativeDir, "a.sql"), - filepath.Join(utils.DeclarativeDir, "nested", "b.sql"), - }, declared) - }) -} - -func TestRun(t *testing.T) { - t.Run("runs migra diff", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, flags.LoadConfig(fsys)) - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), "test-shadow-db") - gock.New(utils.Docker.DaemonHost()). - Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db"). - Reply(http.StatusOK) - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{ - Running: true, - Health: &container.Health{Status: types.Healthy}, - }, - }}) - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Realtime.Image), "test-shadow-realtime") - require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-shadow-realtime", "")) - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Storage.Image), "test-shadow-storage") - require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-shadow-storage", "")) - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Auth.Image), "test-shadow-auth") - require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-shadow-auth", "")) - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.EdgeRuntime.Image), "test-migra") - diff := "create table test();" - require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-migra", diff)) - // Setup mock postgres: with auto_expose_new_tables unset, the shadow database setup - // revokes the default Data API GRANTs before creating the regression template. - conn := pgtest.NewConn() - helper.MockApiPrivilegesRevoke(conn). - Query(CREATE_TEMPLATE). - Reply("CREATE DATABASE") - defer conn.Close(t) - // Run test - err := Run(context.Background(), []string{"public"}, "file", dbConfig, DiffSchemaMigra, fsys, func(cc *pgx.ConnConfig) { - if cc.Host == dbConfig.Host { - // Fake a SSL error when connecting to target database - cc.LookupFunc = func(ctx context.Context, host string) (addrs []string, err error) { - return nil, errors.New("server refused TLS connection") - } - } else { - // Hijack connection to shadow database - conn.Intercept(cc) - } - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) - // Check diff file - files, err := afero.ReadDir(fsys, utils.MigrationsDir) - assert.NoError(t, err) - assert.Equal(t, 1, len(files)) - diffPath := filepath.Join(utils.MigrationsDir, files[0].Name()) - contents, err := afero.ReadFile(fsys, diffPath) - assert.NoError(t, err) - assert.Equal(t, []byte(diff), contents) - }) - - t.Run("throws error on failure to diff target", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Config.Db.Image) + "/json"). - ReplyError(errors.New("network error")) - // Run test - err := Run(context.Background(), []string{"public"}, "file", dbConfig, DiffSchemaMigra, fsys) - // Check error - assert.ErrorContains(t, err, "network error") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} - -func TestMigrateShadow(t *testing.T) { - utils.Config.Db.MajorVersion = 14 - - t.Run("migrates shadow database", func(t *testing.T) { - utils.Config.Db.ShadowPort = 54320 - utils.GlobalsSql = "create schema public" - utils.InitialSchemaPg14Sql = "create schema private" - // Setup in-memory fs - fsys := afero.NewMemMapFs() - path := filepath.Join(utils.MigrationsDir, "0_test.sql") - sql := "create schema test" - require.NoError(t, afero.WriteFile(fsys, path, []byte(sql), 0644)) - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(utils.GlobalsSql). - Reply("CREATE SCHEMA"). - Query(utils.InitialSchemaPg14Sql). - Reply("CREATE SCHEMA") - helper.MockApiPrivilegesRevoke(conn). - Query(CREATE_TEMPLATE). - Reply("CREATE DATABASE") - helper.MockMigrationHistory(conn). - Query("RESET ALL"). - Reply("RESET"). - Query(sql). - Reply("CREATE SCHEMA"). - Query(migration.INSERT_MIGRATION_VERSION, "0", "test", []string{sql}). - Reply("INSERT 0 1") - // Run test - err := MigrateShadowDatabase(context.Background(), "test-shadow-db", fsys, conn.Intercept) - // Check error - assert.NoError(t, err) - }) - - t.Run("throws error on timeout", func(t *testing.T) { - utils.Config.Db.ShadowPort = 54320 - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup cancelled context - ctx, cancel := context.WithCancel(context.Background()) - cancel() - // Run test - err := MigrateShadowDatabase(ctx, "", fsys) - // Check error - assert.ErrorIs(t, err, context.Canceled) - }) - - t.Run("throws error on permission denied", func(t *testing.T) { - // Setup in-memory fs - fsys := &fstest.OpenErrorFs{DenyPath: utils.MigrationsDir} - // Run test - err := MigrateShadowDatabase(context.Background(), "", fsys) - // Check error - assert.ErrorIs(t, err, os.ErrPermission) - }) - - t.Run("throws error on globals schema", func(t *testing.T) { - utils.Config.Db.ShadowPort = 54320 - utils.GlobalsSql = "create schema public" - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(utils.GlobalsSql). - ReplyError(pgerrcode.DuplicateSchema, `schema "public" already exists`) - // Run test - err := MigrateShadowDatabase(context.Background(), "test-shadow-db", fsys, conn.Intercept) - // Check error - assert.ErrorContains(t, err, `ERROR: schema "public" already exists (SQLSTATE 42P06)`) - }) -} - -func TestSetupShadowDatabase(t *testing.T) { - utils.Config.Db.MajorVersion = 14 - - t.Run("sets up platform baseline without applying migrations", func(t *testing.T) { - utils.Config.Db.ShadowPort = 54320 - utils.GlobalsSql = "create schema public" - utils.InitialSchemaPg14Sql = "create schema private" - // A migration exists on disk, but SetupShadowDatabase must not apply it: - // the mock below only scripts the platform setup + template, so any - // migration-history query would surface as an unmatched request. - fsys := afero.NewMemMapFs() - path := filepath.Join(utils.MigrationsDir, "0_test.sql") - require.NoError(t, afero.WriteFile(fsys, path, []byte("create schema test"), 0644)) - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(utils.GlobalsSql). - Reply("CREATE SCHEMA"). - Query(utils.InitialSchemaPg14Sql). - Reply("CREATE SCHEMA") - helper.MockApiPrivilegesRevoke(conn). - Query(CREATE_TEMPLATE). - Reply("CREATE DATABASE") - // Run test - err := SetupShadowDatabase(context.Background(), "test-shadow-db", fsys, conn.Intercept) - // Check error - assert.NoError(t, err) - }) - - t.Run("throws error on globals schema", func(t *testing.T) { - utils.Config.Db.ShadowPort = 54320 - utils.GlobalsSql = "create schema public" - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(utils.GlobalsSql). - ReplyError(pgerrcode.DuplicateSchema, `schema "public" already exists`) - // Run test - err := SetupShadowDatabase(context.Background(), "test-shadow-db", afero.NewMemMapFs(), conn.Intercept) - // Check error - assert.ErrorContains(t, err, `ERROR: schema "public" already exists (SQLSTATE 42P06)`) - }) -} - -func TestDiffDatabase(t *testing.T) { - utils.Config.Db.MajorVersion = 14 - utils.Config.Db.ShadowPort = 54320 - utils.GlobalsSql = "create schema public" - utils.InitialSchemaPg14Sql = "create schema private" - - t.Run("throws error on failure to create shadow", func(t *testing.T) { - errNetwork := errors.New("network error") - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Config.Db.Image) + "/json"). - ReplyError(errNetwork) - // Run test - result, err := DiffDatabase(context.Background(), []string{"public"}, dbConfig, io.Discard, fsys, DiffSchemaMigra) - // Check error - assert.Empty(t, result) - assert.ErrorIs(t, err, errNetwork) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on health check failure", func(t *testing.T) { - utils.Config.Db.HealthTimeout = time.Millisecond - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), "test-shadow-db") - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{ - Running: false, - Status: "exited", - }, - }}) - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db/logs"). - Reply(http.StatusServiceUnavailable) - gock.New(utils.Docker.DaemonHost()). - Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db"). - Reply(http.StatusOK) - // Run test - result, err := DiffDatabase(context.Background(), []string{"public"}, dbConfig, io.Discard, fsys, DiffSchemaMigra) - // Check error - assert.Empty(t, result) - assert.ErrorContains(t, err, "test-shadow-db container is not running: exited") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on failure to migrate shadow", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), "test-shadow-db") - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{ - Running: true, - Health: &container.Health{Status: types.Healthy}, - }, - }}) - gock.New(utils.Docker.DaemonHost()). - Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db"). - Reply(http.StatusOK) - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(utils.GlobalsSql). - ReplyError(pgerrcode.DuplicateSchema, `schema "public" already exists`) - // Run test - result, err := DiffDatabase(context.Background(), []string{"public"}, dbConfig, io.Discard, fsys, DiffSchemaMigra, conn.Intercept) - // Check error - assert.Empty(t, result) - assert.ErrorContains(t, err, `ERROR: schema "public" already exists (SQLSTATE 42P06) -At statement: 0 -create schema public`) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on failure to diff target", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - path := filepath.Join(utils.MigrationsDir, "0_test.sql") - sql := "create schema test" - require.NoError(t, afero.WriteFile(fsys, path, []byte(sql), 0644)) - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), "test-shadow-db") - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{ - Running: true, - Health: &container.Health{Status: types.Healthy}, - }, - }}) - gock.New(utils.Docker.DaemonHost()). - Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db"). - Reply(http.StatusOK) - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.EdgeRuntime.Image), "test-migra") - // The edge-runtime diff waits for the container to exit via inspect before - // reading its logs (it must not follow the log stream — that hangs under - // podman, supabase/pg-toolbelt#312), so the diff failure here surfaces from - // the log read rather than the followed stream. - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/test-migra/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{ExitCode: 0}, - }}) - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/test-migra/logs"). - ReplyError(errors.New("network error")) - gock.New(utils.Docker.DaemonHost()). - Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-migra"). - Reply(http.StatusOK) - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(utils.GlobalsSql). - Reply("CREATE SCHEMA"). - Query(utils.InitialSchemaPg14Sql). - Reply("CREATE SCHEMA") - helper.MockApiPrivilegesRevoke(conn). - Query(CREATE_TEMPLATE). - Reply("CREATE DATABASE") - helper.MockMigrationHistory(conn). - Query("RESET ALL"). - Reply("RESET"). - Query(sql). - Reply("CREATE SCHEMA"). - Query(migration.INSERT_MIGRATION_VERSION, "0", "test", []string{sql}). - Reply("INSERT 0 1") - // Run test - result, err := DiffDatabase(context.Background(), []string{"public"}, dbConfig, io.Discard, fsys, DiffSchemaMigra, func(cc *pgx.ConnConfig) { - if cc.Host == dbConfig.Host { - // Fake a SSL error when connecting to target database - cc.LookupFunc = func(ctx context.Context, host string) (addrs []string, err error) { - return nil, errors.New("server refused TLS connection") - } - } else { - // Hijack connection to shadow database - conn.Intercept(cc) - } - }) - // Check error - assert.Empty(t, result) - assert.ErrorContains(t, err, "error diffing schema") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} - -func TestDropStatements(t *testing.T) { - drops := findDropStatements("create table t(); drop table t; alter table t drop column c") - assert.Equal(t, []string{"drop table t", "alter table t drop column c"}, drops) -} - -func TestLoadSchemas(t *testing.T) { - expected := []string{ - filepath.Join(utils.SchemasDir, "comment", "model.sql"), - filepath.Join(utils.SchemasDir, "model.sql"), - filepath.Join(utils.SchemasDir, "reaction", "dislike", "model.sql"), - filepath.Join(utils.SchemasDir, "reaction", "like", "model.sql"), - } - fsys := afero.NewMemMapFs() - for _, fp := range expected { - require.NoError(t, afero.WriteFile(fsys, fp, nil, 0644)) - } - // Run test - schemas, err := loadDeclaredSchemas(fsys) - // Check error - assert.NoError(t, err) - assert.ElementsMatch(t, expected, schemas) -} - -func TestLoadSchemasSkipsEmptySchemaPathGlobs(t *testing.T) { - fsys := afero.NewMemMapFs() - matched := filepath.Join(utils.SupabaseDirPath, "schemas", "tables", "players.sql") - require.NoError(t, afero.WriteFile(fsys, matched, nil, 0644)) - utils.Config.Db.Migrations.SchemaPaths = []string{ - filepath.Join(utils.SupabaseDirPath, "schemas", "tables", "*.sql"), - filepath.Join(utils.SupabaseDirPath, "schemas", "materialized_views", "*.sql"), - } - t.Cleanup(func() { - utils.Config.Db.Migrations.SchemaPaths = nil - }) - - schemas, err := loadDeclaredSchemas(fsys) - - assert.NoError(t, err) - assert.Equal(t, []string{filepath.ToSlash(matched)}, schemas) -} - -func TestLoadSchemasErrorsOnMissingLiteralSchemaPath(t *testing.T) { - fsys := afero.NewMemMapFs() - utils.Config.Db.Migrations.SchemaPaths = []string{ - filepath.Join(utils.SupabaseDirPath, "schemas", "tables", "players.sql"), - } - t.Cleanup(func() { - utils.Config.Db.Migrations.SchemaPaths = nil - }) - - schemas, err := loadDeclaredSchemas(fsys) - - assert.ErrorContains(t, err, "no files matched pattern") - assert.Empty(t, schemas) -} - -func TestLoadSchemasErrorsWhenAllSchemaPathGlobsAreEmpty(t *testing.T) { - fsys := afero.NewMemMapFs() - utils.Config.Db.Migrations.SchemaPaths = []string{ - filepath.Join(utils.SupabaseDirPath, "schemas", "tables", "*.sql"), - filepath.Join(utils.SupabaseDirPath, "schemas", "views", "*.sql"), - } - t.Cleanup(func() { - utils.Config.Db.Migrations.SchemaPaths = nil - }) - - schemas, err := loadDeclaredSchemas(fsys) - - assert.ErrorContains(t, err, "no files matched pattern") - assert.Empty(t, schemas) -} diff --git a/apps/cli-go/internal/db/diff/migra.go b/apps/cli-go/internal/db/diff/migra.go deleted file mode 100644 index b5c001825b..0000000000 --- a/apps/cli-go/internal/db/diff/migra.go +++ /dev/null @@ -1,162 +0,0 @@ -package diff - -import ( - "bytes" - "context" - _ "embed" - "strings" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/network" - "github.com/go-errors/errors" - "github.com/jackc/pgconn" - "github.com/jackc/pgx/v4" - "github.com/supabase/cli/internal/gen/types" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/config" - "github.com/supabase/cli/pkg/migration" -) - -var ( - //go:embed templates/migra.sh - diffSchemaScript string - //go:embed templates/migra.ts - diffSchemaTypeScript string - - managedSchemas = []string{ - // Local development - "_analytics", - "_realtime", - "_supavisor", - // Owned by extensions - "cron", - "graphql", - "graphql_public", - "net", - "pgroonga", - "pgtle", - "repack", - "tiger_data", - "vault", - // Deprecated extensions - "pgsodium", - "pgsodium_masks", - "timescaledb_experimental", - "timescaledb_information", - "_timescaledb_cache", - "_timescaledb_catalog", - "_timescaledb_config", - "_timescaledb_debug", - "_timescaledb_functions", - "_timescaledb_internal", - // Managed by Supabase - "pgbouncer", - "supabase_functions", - "supabase_migrations", - } -) - -// Diffs local database schema against shadow, dumps output to stdout. -func DiffSchemaMigraBash(ctx context.Context, source, target pgconn.Config, schema []string, options ...func(*pgx.ConnConfig)) (string, error) { - // Load all user defined schemas - if len(schema) == 0 { - var err error - if schema, err = loadSchema(ctx, target, options...); err != nil { - return "", err - } - } - env := []string{ - "SOURCE=" + utils.ToPostgresURL(source), - "TARGET=" + utils.ToPostgresURL(target), - } - if types.IsSSLDebugEnabled() { - env = append(env, "SUPABASE_SSL_DEBUG=true") - } - // Passing in script string means command line args must be set manually, ie. "$@" - args := "set -- " + strings.Join(schema, " ") + ";" - cmd := []string{"/bin/sh", "-c", args + diffSchemaScript} - var out, stderr bytes.Buffer - if err := utils.DockerRunOnceWithConfig( - ctx, - container.Config{ - Image: config.Images.Migra, - Env: env, - Cmd: cmd, - }, - container.HostConfig{ - NetworkMode: network.NetworkHost, - }, - network.NetworkingConfig{}, - "", - &out, - &stderr, - ); err != nil { - return "", errors.Errorf("error diffing schema: %w:\n%s", err, stderr.String()) - } - return out.String(), nil -} - -func loadSchema(ctx context.Context, config pgconn.Config, options ...func(*pgx.ConnConfig)) ([]string, error) { - conn, err := utils.ConnectByConfig(ctx, config, options...) - if err != nil { - return nil, err - } - defer conn.Close(context.Background()) - // RLS policies in auth and storage schemas can be included with -s flag - return migration.ListUserSchemas(ctx, conn) -} - -func DiffSchemaMigra(ctx context.Context, source, target pgconn.Config, schema []string, options ...func(*pgx.ConnConfig)) (string, error) { - env := []string{ - "SOURCE=" + utils.ToPostgresURL(source), - "TARGET=" + utils.ToPostgresURL(target), - } - debugf := func(string, ...any) {} - if types.IsSSLDebugEnabled() { - debugf = types.LogSSLDebugf - env = append(env, "SUPABASE_SSL_DEBUG=true") - debugf("DiffSchemaMigra source_host=%s source_port=%d target_host=%s target_port=%d target_db=%s", - source.Host, - source.Port, - target.Host, - target.Port, - target.Database, - ) - debugf("DiffSchemaMigra docker_daemon=%s image=%s", utils.Docker.DaemonHost(), utils.Config.EdgeRuntime.Image) - } - if ca, err := types.GetRootCA(ctx, utils.ToPostgresURL(target), options...); err != nil { - debugf("DiffSchemaMigra GetRootCA error=%v", err) - return "", err - } else if len(ca) > 0 { - env = append(env, "SSL_CA="+ca) - debugf("DiffSchemaMigra GetRootCA ca_bundle_len=%d", len(ca)) - } - if len(schema) > 0 { - env = append(env, "INCLUDED_SCHEMAS="+strings.Join(schema, ",")) - } else { - env = append(env, "EXCLUDED_SCHEMAS="+strings.Join(managedSchemas, ",")) - } - // Migra also executes via Edge Runtime because the TypeScript implementation - // shares the same containerized execution environment as other diff engines. - // The helper remains in package diff to avoid coupling migra code paths to - // pg-delta-specific packages. - binds := []string{utils.EdgeRuntimeId + ":/root/.cache/deno:rw"} - var stdout, stderr bytes.Buffer - if err := utils.RunEdgeRuntimeScript(ctx, env, diffSchemaTypeScript, binds, "error diffing schema", &stdout, &stderr); err != nil { - if shouldFallbackToLegacyMigra(err) { - debugf("DiffSchemaMigra falling back to legacy migra after edge-runtime OOM") - return DiffSchemaMigraBash(ctx, source, target, schema, options...) - } - return "", err - } - return stdout.String(), nil -} - -func shouldFallbackToLegacyMigra(err error) bool { - if err == nil { - return false - } - message := err.Error() - return strings.Contains(message, "Fatal JavaScript out of memory") || - strings.Contains(message, "Ineffective mark-compacts near heap limit") -} diff --git a/apps/cli-go/internal/db/diff/pgschema.go b/apps/cli-go/internal/db/diff/pgschema.go deleted file mode 100644 index 5126ca96ed..0000000000 --- a/apps/cli-go/internal/db/diff/pgschema.go +++ /dev/null @@ -1,58 +0,0 @@ -package diff - -import ( - "context" - "database/sql" - "fmt" - "strings" - - "github.com/go-errors/errors" - "github.com/jackc/pgconn" - "github.com/jackc/pgx/v4" - pgschema "github.com/stripe/pg-schema-diff/pkg/diff" - "github.com/supabase/cli/internal/utils" -) - -func DiffPgSchema(ctx context.Context, source, target pgconn.Config, schema []string, _ ...func(*pgx.ConnConfig)) (string, error) { - dbSrc, err := sql.Open("pgx", utils.ToPostgresURL(source)) - if err != nil { - return "", errors.Errorf("failed to open source database: %w", err) - } - defer dbSrc.Close() - dbDst, err := sql.Open("pgx", utils.ToPostgresURL(target)) - if err != nil { - return "", errors.Errorf("failed to open target database: %w", err) - } - defer dbDst.Close() - // Generate DDL based on schema plan - opts := []pgschema.PlanOpt{pgschema.WithDoNotValidatePlan()} - if len(schema) > 0 { - opts = append(opts, pgschema.WithIncludeSchemas(schema...)) - } else { - opts = append(opts, - pgschema.WithExcludeSchemas(managedSchemas...), - pgschema.WithExcludeSchemas( - "topology", // unsupported due to views - "realtime", // unsupported due to partitioned table - "storage", // unsupported due to unique index - ), - ) - } - plan, err := pgschema.Generate( - ctx, - pgschema.DBSchemaSource(dbSrc), - pgschema.DBSchemaSource(dbDst), - opts..., - ) - if err != nil { - return "", errors.Errorf("failed to generate plan: %w", err) - } - var lines []string - for _, stat := range plan.Statements { - for _, harzard := range stat.Hazards { - lines = append(lines, fmt.Sprintf("-- %s", harzard)) - } - lines = append(lines, fmt.Sprintf("%s;\n", stat.DDL)) - } - return fmt.Sprintln(strings.Join(lines, "\n")), nil -} diff --git a/apps/cli-go/internal/db/diff/save_diff_test.go b/apps/cli-go/internal/db/diff/save_diff_test.go deleted file mode 100644 index e03df73eec..0000000000 --- a/apps/cli-go/internal/db/diff/save_diff_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package diff - -import ( - "testing" - - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/utils" -) - -func TestSaveDiff(t *testing.T) { - t.Run("reports no changes on empty diff", func(t *testing.T) { - fsys := afero.NewMemMapFs() - require.NoError(t, SaveDiff(DatabaseDiff{SQL: ""}, "my_diff", fsys)) - entries, err := afero.ReadDir(fsys, utils.MigrationsDir) - assert.Error(t, err) - assert.Empty(t, entries) - }) - - t.Run("writes a single migration file", func(t *testing.T) { - fsys := afero.NewMemMapFs() - require.NoError(t, SaveDiff(DatabaseDiff{SQL: "create table a ();"}, "my_diff", fsys)) - entries, err := afero.ReadDir(fsys, utils.MigrationsDir) - require.NoError(t, err) - require.Len(t, entries, 1) - assert.Regexp(t, `^\d{14}_my_diff\.sql$`, entries[0].Name()) - contents, err := afero.ReadFile(fsys, utils.MigrationsDir+"/"+entries[0].Name()) - require.NoError(t, err) - assert.Equal(t, "create table a ();", string(contents)) - }) - - t.Run("prints diff to stdout when no file is given", func(t *testing.T) { - fsys := afero.NewMemMapFs() - require.NoError(t, SaveDiff(DatabaseDiff{SQL: "create table a ();"}, "", fsys)) - entries, _ := afero.ReadDir(fsys, utils.MigrationsDir) - assert.Empty(t, entries) - }) - - t.Run("creates nested parent directories for a nested name", func(t *testing.T) { - fsys := afero.NewMemMapFs() - require.NoError(t, SaveDiff(DatabaseDiff{SQL: "create table a ();"}, "snapshots/remote", fsys)) - matches, err := afero.Glob(fsys, utils.MigrationsDir+"/*_snapshots/remote.sql") - require.NoError(t, err) - require.Len(t, matches, 1) - contents, err := afero.ReadFile(fsys, matches[0]) - require.NoError(t, err) - assert.Equal(t, "create table a ();", string(contents)) - }) -} diff --git a/apps/cli-go/internal/db/diff/shadow.go b/apps/cli-go/internal/db/diff/shadow.go deleted file mode 100644 index 7cafbeaf1e..0000000000 --- a/apps/cli-go/internal/db/diff/shadow.go +++ /dev/null @@ -1,74 +0,0 @@ -package diff - -import ( - "context" - - "github.com/jackc/pgconn" - "github.com/jackc/pgx/v4" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/db/start" - "github.com/supabase/cli/internal/utils" -) - -// ShadowSource is a provisioned shadow database, left running for an external -// caller to diff against and then remove. It mirrors the shadow that -// DiffDatabase prepares as the diff "source". -type ShadowSource struct { - // Container is the shadow database container id; the caller MUST remove it - // (e.g. `docker rm -f `) when the diff completes. - Container string - // Source is the connection config for the diff source (the shadow with the - // platform baseline + local migrations applied). - Source pgconn.Config - // TargetOverride, when non-nil, replaces the diff target with a second - // shadow database (contrib_regression with declarative schemas applied). - TargetOverride *pgconn.Config -} - -// PrepareShadowSource provisions the shadow database that DiffDatabase diffs -// against, but returns it running instead of diffing + removing. targetLocal -// mirrors utils.IsLocalDatabase(config). On error the shadow container is -// removed. Declared schemas are applied with the migra seed path; pg-delta -// apply lives in the TypeScript CLI. -func PrepareShadowSource(ctx context.Context, targetLocal bool, fsys afero.Fs, options ...func(*pgx.ConnConfig)) (ShadowSource, error) { - shadow, err := CreateShadowDatabase(ctx, utils.Config.Db.ShadowPort) - if err != nil { - return ShadowSource{}, err - } - ok := false - defer func() { - if !ok { - utils.DockerRemove(shadow) - } - }() - if err := start.WaitForHealthyService(ctx, utils.Config.Db.HealthTimeout, shadow); err != nil { - return ShadowSource{}, err - } - if err := MigrateShadowDatabase(ctx, shadow, fsys, options...); err != nil { - return ShadowSource{}, err - } - shadowConfig := pgconn.Config{ - Host: utils.Config.Hostname, - Port: utils.Config.Db.ShadowPort, - User: "postgres", - Password: utils.Config.Db.Password, - Database: "postgres", - } - var targetOverride *pgconn.Config - if targetLocal { - declared, err := loadDeclaredSchemas(fsys) - if err != nil { - return ShadowSource{}, err - } - if len(declared) > 0 { - override := shadowConfig - override.Database = "contrib_regression" - if err := migrateBaseDatabase(ctx, override, declared, fsys, options...); err != nil { - return ShadowSource{}, err - } - targetOverride = &override - } - } - ok = true - return ShadowSource{Container: shadow, Source: shadowConfig, TargetOverride: targetOverride}, nil -} diff --git a/apps/cli-go/internal/db/diff/templates/migra.sh b/apps/cli-go/internal/db/diff/templates/migra.sh deleted file mode 100755 index e630bd1147..0000000000 --- a/apps/cli-go/internal/db/diff/templates/migra.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh -set -eu - -if [ "${SUPABASE_SSL_DEBUG:-}" = "true" ]; then - [ -n "${SOURCE:-}" ] && source_set=true || source_set=false - [ -n "${TARGET:-}" ] && target_set=true || target_set=false - echo "[ssl-debug] migra.sh uname=$(uname -a)" >&2 - echo "[ssl-debug] migra.sh source_set=$source_set target_set=$target_set schemas=$*" >&2 -fi - -# migra doesn't shutdown gracefully, so kill it ourselves -trap 'kill -9 %1' TERM - -run_migra() { - # additional flags for diffing extensions - [ "$schema" = "extensions" ] && set -- --create-extensions-only --ignore-extension-versions "$@" - migra --with-privileges --unsafe --schema="$schema" "$@" -} - -# accepts command line args as a list of schema to generate -for schema in "$@"; do - # migra exits 2 when differences are found - run_migra "$SOURCE" "$TARGET" || status=$? - if [ "${SUPABASE_SSL_DEBUG:-}" = "true" ]; then - echo "[ssl-debug] migra.sh schema=$schema exit_status=${status:-0}" >&2 - fi - if [ ${status:-2} -ne 2 ]; then - exit $status - fi -done diff --git a/apps/cli-go/internal/db/diff/templates/migra.ts b/apps/cli-go/internal/db/diff/templates/migra.ts deleted file mode 100644 index d67124140f..0000000000 --- a/apps/cli-go/internal/db/diff/templates/migra.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { createClient } from "npm:@pgkit/client"; -import { Migration } from "npm:@pgkit/migra"; - -// Avoids error on self-signed certificate -const ca = Deno.env.get("SSL_CA"); -const source = Deno.env.get("SOURCE"); -const target = Deno.env.get("TARGET"); -const sslDebug = Deno.env.get("SUPABASE_SSL_DEBUG")?.toLowerCase() === "true"; - -function redactPostgresUrl(raw: string | undefined): string { - if (!raw) return ""; - try { - const u = new URL(raw); - if (u.password) u.password = "xxxxx"; - return u.toString(); - } catch { - return ""; - } -} - -if (sslDebug) { - console.error( - `[ssl-debug] migra.ts deno=${Deno.version.deno} v8=${Deno.version.v8} os=${Deno.build.os}`, - ); - console.error( - `[ssl-debug] migra.ts source=${redactPostgresUrl(source)} target=${redactPostgresUrl(target)}`, - ); - console.error( - `[ssl-debug] migra.ts ssl_ca_set=${ca != null} ssl_ca_len=${ca?.length ?? 0}`, - ); -} - -type PoolClient = { - query: (stmt: string) => Promise; - on: (event: "error", listener: () => void) => void; - off: (event: "error", listener: () => void) => void; -}; -// Step down from login role to postgres and force schema qualified references for -// pg_get_expr on every pooled connection, including one reopened after an idle timeout -const verify = (stmt: string) => (client: PoolClient, done: (err?: Error) => void) => { - // pg-pool drops its error listener during verify, so a socket error must reject, not throw - const ignore = () => {}; - client.on("error", ignore); - client - .query(stmt) - .finally(() => client.off("error", ignore)) - .then( - () => done(), - (err: Error) => { - err.message = `${stmt}: ${err.message}`; - done(err); - }, - ); -}; -const clientBase = createClient(source, { - pgpOptions: { connect: { verify: verify("set search_path = ''") } }, -}); -const clientHead = createClient(target, { - pgpOptions: { - connect: { - ssl: ca && { ca }, - verify: verify("set role postgres; set search_path = ''"), - }, - }, -}); -const includedSchemas = Deno.env.get("INCLUDED_SCHEMAS")?.split(",") ?? []; -const excludedSchemas = Deno.env.get("EXCLUDED_SCHEMAS")?.split(",") ?? []; - -const managedSchemas = ["auth", "realtime", "storage"]; -const extensionSchemas = [ - "pg_catalog", - "extensions", - "pgmq", - "tiger", - "topology", -]; - -try { - const result: string[] = []; - for (const schema of includedSchemas) { - const m = await Migration.create(clientBase, clientHead, { - schema, - ignore_extension_versions: true, - }); - m.set_safety(false); - if (managedSchemas.includes(schema)) { - m.add(m.changes.triggers({ drops_only: true })); - m.add(m.changes.rlspolicies({ drops_only: true })); - m.add(m.changes.rlspolicies({ creations_only: true })); - m.add(m.changes.triggers({ creations_only: true })); - } else { - m.add_all_changes(true); - } - result.push(m.sql); - } - if (includedSchemas.length === 0) { - // Migra does not ignore custom types and triggers created by extensions, so we diff - // them separately. This workaround only applies to a known list of managed schemas. - for (const schema of extensionSchemas) { - const e = await Migration.create(clientBase, clientHead, { - schema, - ignore_extension_versions: true, - }); - e.set_safety(false); - e.add(e.changes.schemas({ creations_only: true })); - e.add_extension_changes(); - result.push(e.sql); - } - // Diff user defined entities in non-managed schemas, including extensions. - const m = await Migration.create(clientBase, clientHead, { - exclude_schema: [ - ...managedSchemas, - ...extensionSchemas, - ...excludedSchemas, - ], - ignore_extension_versions: true, - }); - m.set_safety(false); - m.add_all_changes(true); - result.push(m.sql); - // For managed schemas, we want to include triggers and RLS policies only. - for (const schema of managedSchemas) { - const s = await Migration.create(clientBase, clientHead, { - schema, - ignore_extension_versions: true, - }); - s.set_safety(false); - s.add(s.changes.triggers({ drops_only: true })); - s.add(s.changes.rlspolicies({ drops_only: true })); - s.add(s.changes.rlspolicies({ creations_only: true })); - s.add(s.changes.triggers({ creations_only: true })); - result.push(s.sql); - } - } - console.log(result.join("")); -} catch (e) { - if (sslDebug) { - if (e instanceof Error) { - console.error( - `[ssl-debug] migra.ts error_name=${e.name} message=${e.message} stack=${e.stack ?? ""}`, - ); - } else { - console.error(`[ssl-debug] migra.ts error=${String(e)}`); - } - } - console.error(e); - console.error("PGDELTA_SCRIPT_ERROR"); -} finally { - await Promise.all([clientHead.end(), clientBase.end()]); -} diff --git a/apps/cli-go/internal/db/reset/reset.go b/apps/cli-go/internal/db/reset/reset.go deleted file mode 100644 index 7ac3f8e4f1..0000000000 --- a/apps/cli-go/internal/db/reset/reset.go +++ /dev/null @@ -1,313 +0,0 @@ -package reset - -import ( - "bytes" - "context" - _ "embed" - "fmt" - "io" - "os" - "strconv" - "strings" - "time" - - "github.com/cenkalti/backoff/v4" - "github.com/containerd/errdefs" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/network" - "github.com/go-errors/errors" - "github.com/jackc/pgconn" - "github.com/jackc/pgerrcode" - "github.com/jackc/pgx/v4" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/db/start" - "github.com/supabase/cli/internal/migration/apply" - "github.com/supabase/cli/internal/migration/down" - "github.com/supabase/cli/internal/migration/list" - "github.com/supabase/cli/internal/migration/repair" - "github.com/supabase/cli/internal/seed/buckets" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/migration" -) - -func Run(ctx context.Context, version string, last uint, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { - if len(version) > 0 { - if _, err := strconv.Atoi(version); err != nil { - return errors.New(repair.ErrInvalidVersion) - } - if _, err := repair.GetMigrationFile(version, fsys); err != nil { - return err - } - } else if last > 0 { - localMigrations, err := list.LoadLocalVersions(fsys) - if err != nil { - return err - } - if total := uint(len(localMigrations)); last < total { - version = localMigrations[total-last-1] - } else { - // Negative skips all migrations - version = "-" - } - } - if !utils.IsLocalDatabase(config) { - return resetRemote(ctx, version, config, fsys, options...) - } - // Config file is loaded before parsing --linked or --local flags - if err := utils.AssertSupabaseDbIsRunning(); err != nil { - return err - } - // Reset postgres database because extensions (pg_cron, pg_net) require postgres - if err := resetDatabase(ctx, version, fsys, options...); err != nil { - return err - } - // Seed objects from supabase/buckets directory - if resp, err := utils.Docker.ContainerInspect(ctx, utils.StorageId); err == nil { - if resp.State.Health == nil || resp.State.Health.Status != types.Healthy { - if err := start.WaitForHealthyService(ctx, 30*time.Second, utils.StorageId); err != nil { - return err - } - } - if err := buckets.Run(ctx, "", false, fsys); err != nil { - return err - } - } - branch := utils.GetGitBranch(fsys) - fmt.Fprintln(os.Stderr, "Finished "+utils.Aqua("supabase db reset")+" on branch "+utils.Aqua(branch)+".") - return nil -} - -func resetDatabase(ctx context.Context, version string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { - fmt.Fprintln(os.Stderr, "Resetting local database"+toLogMessage(version)) - if utils.Config.Db.MajorVersion <= 14 { - return resetDatabase14(ctx, version, fsys, options...) - } - return resetDatabase15(ctx, version, fsys, options...) -} - -func toLogMessage(version string) string { - if len(version) > 0 { - return " to version: " + version - } - return "..." -} - -func resetDatabase14(ctx context.Context, version string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { - if err := recreateDatabase(ctx, options...); err != nil { - return err - } - if err := initDatabase(ctx, options...); err != nil { - return err - } - if err := RestartDatabase(ctx, os.Stderr); err != nil { - return err - } - conn, err := utils.ConnectLocalPostgres(ctx, pgconn.Config{}, options...) - if err != nil { - return err - } - defer conn.Close(context.Background()) - return apply.MigrateAndSeed(ctx, version, conn, fsys) -} - -func resetDatabase15(ctx context.Context, version string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { - if err := utils.Docker.ContainerRemove(ctx, utils.DbId, container.RemoveOptions{Force: true}); err != nil { - return errors.Errorf("failed to remove container: %w", err) - } - if err := utils.Docker.VolumeRemove(ctx, utils.DbId, true); err != nil { - return errors.Errorf("failed to remove volume: %w", err) - } - config := start.NewContainerConfig() - hostConfig := start.NewHostConfig() - networkingConfig := network.NetworkingConfig{ - EndpointsConfig: map[string]*network.EndpointSettings{ - utils.NetId: { - Aliases: utils.DbAliases, - }, - }, - } - fmt.Fprintln(os.Stderr, "Recreating database...") - if _, err := utils.DockerStart(ctx, config, hostConfig, networkingConfig, utils.DbId); err != nil { - return err - } - if err := start.WaitForHealthyService(ctx, utils.Config.Db.HealthTimeout, utils.DbId); err != nil { - return err - } - if err := start.SetupLocalDatabase(ctx, version, fsys, os.Stderr, options...); err != nil { - return err - } - fmt.Fprintln(os.Stderr, "Restarting containers...") - return restartServices(ctx) -} - -func initDatabase(ctx context.Context, options ...func(*pgx.ConnConfig)) error { - conn, err := utils.ConnectLocalPostgres(ctx, pgconn.Config{User: utils.SUPERUSER_ROLE}, options...) - if err != nil { - return err - } - defer conn.Close(context.Background()) - if err := start.InitSchema14(ctx, conn); err != nil { - return err - } - return start.ApplyApiPrivileges(ctx, conn) -} - -// Recreate postgres database by connecting to template1 -func recreateDatabase(ctx context.Context, options ...func(*pgx.ConnConfig)) error { - conn, err := utils.ConnectLocalPostgres(ctx, pgconn.Config{User: utils.SUPERUSER_ROLE, Database: "template1"}, options...) - if err != nil { - return err - } - defer conn.Close(context.Background()) - if err := DisconnectClients(ctx, conn); err != nil { - return err - } - // We are not dropping roles here because they are cluster level entities. Use stop && start instead. - sql := migration.MigrationFile{ - Statements: []string{ - "DROP DATABASE IF EXISTS postgres WITH (FORCE)", - "CREATE DATABASE postgres WITH OWNER postgres", - "DROP DATABASE IF EXISTS _supabase WITH (FORCE)", - "CREATE DATABASE _supabase WITH OWNER postgres", - }, - } - return sql.ExecBatch(ctx, conn) -} - -const ( - TERMINATE_BACKENDS = "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname IN ('postgres', '_supabase')" - COUNT_REPLICATION_SLOTS = "SELECT COUNT(*) FROM pg_replication_slots WHERE database IN ('postgres', '_supabase')" -) - -func DisconnectClients(ctx context.Context, conn *pgx.Conn) error { - // Must be executed separately because looping in transaction is unsupported - // https://dba.stackexchange.com/a/11895 - disconn := migration.MigrationFile{ - Statements: []string{ - "ALTER DATABASE postgres ALLOW_CONNECTIONS false", - "ALTER DATABASE _supabase ALLOW_CONNECTIONS false", - TERMINATE_BACKENDS, - }, - } - if err := disconn.ExecBatch(ctx, conn); err != nil { - var pgErr *pgconn.PgError - if errors.As(err, &pgErr) && pgErr.Code != pgerrcode.InvalidCatalogName { - return errors.Errorf("failed to disconnect clients: %w", err) - } - } - // Wait for WAL senders to drop their replication slots - policy := start.NewBackoffPolicy(ctx, 10*time.Second) - waitForDrop := func() error { - var count int - if err := conn.QueryRow(ctx, COUNT_REPLICATION_SLOTS).Scan(&count); err != nil { - err = errors.Errorf("failed to count replication slots: %w", err) - return &backoff.PermanentError{Err: err} - } else if count > 0 { - return errors.Errorf("replication slots still active: %d", count) - } - return nil - } - return backoff.Retry(waitForDrop, policy) -} - -func RestartDatabase(ctx context.Context, w io.Writer) error { - fmt.Fprintln(w, "Restarting containers...") - // Some extensions must be manually restarted after pg_terminate_backend - // Ref: https://github.com/citusdata/pg_cron/issues/99 - if err := utils.Docker.ContainerRestart(ctx, utils.DbId, container.StopOptions{}); err != nil { - return errors.Errorf("failed to restart container: %w", err) - } - if err := start.WaitForHealthyService(ctx, utils.Config.Db.HealthTimeout, utils.DbId); err != nil { - return err - } - return restartServices(ctx) -} - -func restartServices(ctx context.Context) error { - // No need to restart PostgREST because it automatically reconnects and listens for schema changes - services := listServicesToRestart() - result := utils.WaitAll(services, func(id string) error { - if err := utils.Docker.ContainerRestart(ctx, id, container.StopOptions{}); err != nil && !errdefs.IsNotFound(err) { - return errors.Errorf("failed to restart %s: %w", id, err) - } - return nil - }) - // Do not wait for service healthy as those services may be excluded from starting - if err := errors.Join(result...); err != nil { - return err - } - return reloadKong(ctx) -} - -func listServicesToRestart() []string { - return []string{utils.StorageId, utils.GotrueId, utils.RealtimeId, utils.PoolerId} -} - -// reloadKong reloads Kong after the restarts above so its nginx re-resolves each -// upstream container's address. Kong caches resolved addresses for the life of a -// worker process, and a restarted container can come back on a different one, -// leaving the gateway returning 502 for that route until Kong restarts. An -// in-place reload (the `functions serve` pattern) keeps the gateway serving -// throughout. https://github.com/supabase/cli/issues/6016 -func reloadKong(ctx context.Context) error { - resp, err := utils.Docker.ContainerInspect(ctx, utils.KongId) - if errdefs.IsNotFound(err) { - // Kong may be excluded from the stack. - return nil - } else if err != nil { - return suggestKongRecovery(errors.Errorf("failed to inspect kong: %w", err)) - } - if !resp.State.Running { - // A stopped gateway has no stale cache to flush. - return nil - } - var out bytes.Buffer - // Reload re-renders nginx.conf from Kong's default template, so it needs the - // template bring-up wrote (start.go:589-592) handed back — otherwise it drops - // that template's email_templates server. https://github.com/supabase/cli/issues/6059 - if err := utils.DockerExecOnceWithStream(ctx, utils.KongId, "", nil, []string{"kong", "reload", "--nginx-conf", "/home/kong/custom_nginx.template"}, &out, &out); err != nil { - if msg := strings.TrimSpace(out.String()); len(msg) > 0 { - return suggestKongRecovery(errors.Errorf("failed to reload kong: %w:\n%s", err, msg)) - } - return suggestKongRecovery(errors.Errorf("failed to reload kong: %w", err)) - } - return nil -} - -// suggestKongRecovery decorates a gateway-left-unconfirmed failure with the -// advisory next step; the caller-neutral wording also covers branch switch, -// which shares RestartDatabase. -func suggestKongRecovery(err error) error { - utils.CmdSuggestion = fmt.Sprintf( - "Local services restarted, but API routes may return 502 until the gateway reloads.\nTry restarting it with %s, and check %s if the failure persists.", - utils.Aqua("docker restart "+utils.KongId), - utils.Aqua("docker logs "+utils.KongId), - ) - return err -} - -func resetRemote(ctx context.Context, version string, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { - msg := "Do you want to reset the remote database?" - if shouldReset, err := utils.NewConsole().PromptYesNo(ctx, msg, false); err != nil { - return err - } else if !shouldReset { - return errors.New(context.Canceled) - } - fmt.Fprintln(os.Stderr, "Resetting remote database"+toLogMessage(version)) - conn, err := utils.ConnectByConfigStream(ctx, config, io.Discard, options...) - if err != nil { - return err - } - defer conn.Close(context.Background()) - return down.ResetAll(ctx, version, conn, fsys) -} - -func LikeEscapeSchema(schemas []string) (result []string) { - // Treat _ as literal, * as any character - replacer := strings.NewReplacer("_", `\_`, "*", "%") - for _, sch := range schemas { - result = append(result, replacer.Replace(sch)) - } - return result -} diff --git a/apps/cli-go/internal/db/reset/reset_test.go b/apps/cli-go/internal/db/reset/reset_test.go deleted file mode 100644 index 5d672ec15f..0000000000 --- a/apps/cli-go/internal/db/reset/reset_test.go +++ /dev/null @@ -1,568 +0,0 @@ -package reset - -import ( - "context" - "encoding/json" - "errors" - "io" - "net/http" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/h2non/gock" - "github.com/jackc/pgconn" - "github.com/jackc/pgerrcode" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/testing/apitest" - "github.com/supabase/cli/internal/testing/fstest" - "github.com/supabase/cli/internal/testing/helper" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/pgtest" - "github.com/supabase/cli/pkg/storage" -) - -func TestResetCommand(t *testing.T) { - utils.Config.Hostname = "127.0.0.1" - utils.Config.Db.Port = 5432 - - var dbConfig = pgconn.Config{ - Host: utils.Config.Hostname, - Port: utils.Config.Db.Port, - User: "admin", - Password: "password", - Database: "postgres", - } - - t.Run("seeds storage after reset", func(t *testing.T) { - utils.DbId = "test-reset" - utils.Config.Db.MajorVersion = 15 - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId). - Reply(http.StatusOK). - JSON(container.InspectResponse{}) - gock.New(utils.Docker.DaemonHost()). - Delete("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId). - Reply(http.StatusOK) - gock.New(utils.Docker.DaemonHost()). - Delete("/v" + utils.Docker.ClientVersion() + "/volumes/" + utils.DbId). - Reply(http.StatusOK) - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), utils.DbId) - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{ - Running: true, - Health: &container.Health{Status: types.Healthy}, - }, - }}) - // Setup mock postgres: with auto_expose_new_tables unset, the default Data API GRANTs - // are revoked by default during database setup. - conn := pgtest.NewConn() - defer conn.Close(t) - helper.MockApiPrivilegesRevoke(conn) - // Restarts services - utils.StorageId = "test-storage" - utils.GotrueId = "test-auth" - utils.RealtimeId = "test-realtime" - utils.PoolerId = "test-pooler" - utils.KongId = "test-kong" - for _, container := range listServicesToRestart() { - gock.New(utils.Docker.DaemonHost()). - Post("/v" + utils.Docker.ClientVersion() + "/containers/" + container + "/restart"). - Reply(http.StatusOK) - } - // Kong is not running so the reload is skipped (a successful exec attach is not gock-mockable, see TestExecOnce) - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.KongId + "/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{Running: false}, - }}) - // Seeds storage - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.StorageId + "/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{ - Running: true, - Health: &container.Health{Status: types.Healthy}, - }, - }}) - gock.New(utils.Config.Api.ExternalUrl). - Get("/storage/v1/bucket"). - Reply(http.StatusOK). - JSON([]storage.BucketResponse{}) - // Run test - err := Run(context.Background(), "", 0, dbConfig, fsys, conn.Intercept) - // Check error - assert.NoError(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on context canceled", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Run test - err := Run(context.Background(), "", 0, pgconn.Config{Host: "db.supabase.co"}, fsys) - // Check error - assert.ErrorIs(t, err, context.Canceled) - }) - - t.Run("throws error on invalid port", func(t *testing.T) { - t.Cleanup(fstest.MockStdin(t, "y")) - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Run test - err := Run(context.Background(), "", 0, pgconn.Config{Host: "db.supabase.co"}, fsys) - // Check error - assert.ErrorContains(t, err, "invalid port (outside range)") - }) - - t.Run("throws error on db is not started", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers"). - Reply(http.StatusNotFound) - // Run test - err := Run(context.Background(), "", 0, dbConfig, fsys) - // Check error - assert.ErrorIs(t, err, utils.ErrNotRunning) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on failure to recreate", func(t *testing.T) { - utils.DbId = "test-reset" - utils.Config.Db.MajorVersion = 15 - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId). - Reply(http.StatusOK). - JSON(container.InspectResponse{}) - gock.New(utils.Docker.DaemonHost()). - Delete("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId). - ReplyError(errors.New("network error")) - // Run test - err := Run(context.Background(), "", 0, dbConfig, fsys) - // Check error - assert.ErrorContains(t, err, "network error") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} - -func TestInitDatabase(t *testing.T) { - t.Run("initializes postgres database", func(t *testing.T) { - utils.Config.Db.Port = 54322 - utils.InitialSchemaPg14Sql = "create schema private" - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(utils.InitialSchemaPg14Sql). - Reply("CREATE SCHEMA") - helper.MockApiPrivilegesRevoke(conn) - // Run test - assert.NoError(t, initDatabase(context.Background(), conn.Intercept)) - }) - - t.Run("throws error on connect failure", func(t *testing.T) { - utils.Config.Db.Port = 0 - // Run test - err := initDatabase(context.Background()) - // Check error - assert.ErrorContains(t, err, "invalid port (outside range)") - }) - - t.Run("throws error on duplicate schema", func(t *testing.T) { - utils.Config.Db.Port = 54322 - utils.InitialSchemaPg14Sql = "create schema private" - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(utils.InitialSchemaPg14Sql). - ReplyError(pgerrcode.DuplicateSchema, `schema "public" already exists`) - // Run test - err := initDatabase(context.Background(), conn.Intercept) - // Check error - assert.ErrorContains(t, err, `ERROR: schema "public" already exists (SQLSTATE 42P06)`) - }) -} - -func TestRecreateDatabase(t *testing.T) { - t.Run("resets postgres database", func(t *testing.T) { - utils.Config.Db.Port = 54322 - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query("ALTER DATABASE postgres ALLOW_CONNECTIONS false"). - Reply("ALTER DATABASE"). - Query("ALTER DATABASE _supabase ALLOW_CONNECTIONS false"). - Reply("ALTER DATABASE"). - Query(TERMINATE_BACKENDS). - Reply("SELECT 1"). - Query(COUNT_REPLICATION_SLOTS). - Reply("SELECT 1", []any{0}). - Query("DROP DATABASE IF EXISTS postgres WITH (FORCE)"). - Reply("DROP DATABASE"). - Query("CREATE DATABASE postgres WITH OWNER postgres"). - Reply("CREATE DATABASE"). - Query("DROP DATABASE IF EXISTS _supabase WITH (FORCE)"). - Reply("DROP DATABASE"). - Query("CREATE DATABASE _supabase WITH OWNER postgres"). - Reply("CREATE DATABASE") - // Run test - assert.NoError(t, recreateDatabase(context.Background(), conn.Intercept)) - }) - - t.Run("throws error on invalid port", func(t *testing.T) { - utils.Config.Db.Port = 0 - assert.ErrorContains(t, recreateDatabase(context.Background()), "invalid port") - }) - - t.Run("continues on disconnecting missing database", func(t *testing.T) { - utils.Config.Db.Port = 54322 - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query("ALTER DATABASE postgres ALLOW_CONNECTIONS false"). - Reply("ALTER DATABASE"). - Query("ALTER DATABASE _supabase ALLOW_CONNECTIONS false"). - ReplyError(pgerrcode.InvalidCatalogName, `database "_supabase" does not exist`). - Query(TERMINATE_BACKENDS). - Query(COUNT_REPLICATION_SLOTS). - ReplyError(pgerrcode.UndefinedTable, `relation "pg_replication_slots" does not exist`) - // Run test - err := recreateDatabase(context.Background(), conn.Intercept) - // Check error - assert.ErrorContains(t, err, `ERROR: relation "pg_replication_slots" does not exist (SQLSTATE 42P01)`) - }) - - t.Run("throws error on failure to disconnect", func(t *testing.T) { - utils.Config.Db.Port = 54322 - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query("ALTER DATABASE postgres ALLOW_CONNECTIONS false"). - ReplyError(pgerrcode.InvalidParameterValue, `cannot disallow connections for current database`). - Query("ALTER DATABASE _supabase ALLOW_CONNECTIONS false"). - Query(TERMINATE_BACKENDS) - // Run test - err := recreateDatabase(context.Background(), conn.Intercept) - // Check error - assert.ErrorContains(t, err, "ERROR: cannot disallow connections for current database (SQLSTATE 22023)") - }) - - t.Run("throws error on failure to drop", func(t *testing.T) { - utils.Config.Db.Port = 54322 - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query("ALTER DATABASE postgres ALLOW_CONNECTIONS false"). - Reply("ALTER DATABASE"). - Query("ALTER DATABASE _supabase ALLOW_CONNECTIONS false"). - Reply("ALTER DATABASE"). - Query(TERMINATE_BACKENDS). - Reply("SELECT 1"). - Query(COUNT_REPLICATION_SLOTS). - Reply("SELECT 1", []any{0}). - Query("DROP DATABASE IF EXISTS postgres WITH (FORCE)"). - ReplyError(pgerrcode.ObjectInUse, `database "postgres" is used by an active logical replication slot`). - Query("CREATE DATABASE postgres WITH OWNER postgres"). - Query("DROP DATABASE IF EXISTS _supabase WITH (FORCE)"). - Reply("DROP DATABASE"). - Query("CREATE DATABASE _supabase WITH OWNER postgres"). - Reply("CREATE DATABASE") - err := recreateDatabase(context.Background(), conn.Intercept) - // Check error - assert.ErrorContains(t, err, `ERROR: database "postgres" is used by an active logical replication slot (SQLSTATE 55006)`) - }) -} - -func TestRestartDatabase(t *testing.T) { - t.Run("restarts affected services", func(t *testing.T) { - utils.DbId = "test-reset" - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - // Restarts postgres - gock.New(utils.Docker.DaemonHost()). - Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/restart"). - Reply(http.StatusOK) - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{ - Running: true, - Health: &container.Health{Status: types.Healthy}, - }, - }}) - // Restarts services - utils.StorageId = "test-storage" - utils.GotrueId = "test-auth" - utils.RealtimeId = "test-realtime" - utils.PoolerId = "test-pooler" - utils.KongId = "test-kong" - for _, container := range listServicesToRestart() { - gock.New(utils.Docker.DaemonHost()). - Post("/v" + utils.Docker.ClientVersion() + "/containers/" + container + "/restart"). - Reply(http.StatusOK) - } - // Kong is not running so the reload is skipped (a successful exec attach is not gock-mockable, see TestExecOnce) - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.KongId + "/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{Running: false}, - }}) - // Run test - err := RestartDatabase(context.Background(), io.Discard) - // Check error - assert.NoError(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on service restart failure", func(t *testing.T) { - utils.DbId = "test-reset" - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - // Restarts postgres - gock.New(utils.Docker.DaemonHost()). - Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/restart"). - Reply(http.StatusOK) - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{ - Running: true, - Health: &container.Health{Status: types.Healthy}, - }, - }}) - // Restarts services - utils.StorageId = "test-storage" - utils.GotrueId = "test-auth" - utils.RealtimeId = "test-realtime" - utils.PoolerId = "test-pooler" - for _, container := range []string{utils.StorageId, utils.GotrueId, utils.RealtimeId} { - gock.New(utils.Docker.DaemonHost()). - Post("/v" + utils.Docker.ClientVersion() + "/containers/" + container + "/restart"). - Reply(http.StatusServiceUnavailable) - } - gock.New(utils.Docker.DaemonHost()). - Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.PoolerId + "/restart"). - Reply(http.StatusNotFound) - // Run test - err := RestartDatabase(context.Background(), io.Discard) - // Check error - assert.ErrorContains(t, err, "failed to restart "+utils.StorageId) - assert.ErrorContains(t, err, "failed to restart "+utils.GotrueId) - assert.ErrorContains(t, err, "failed to restart "+utils.RealtimeId) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("skips kong reload when kong is not running", func(t *testing.T) { - utils.DbId = "test-reset" - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - // Restarts postgres - gock.New(utils.Docker.DaemonHost()). - Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/restart"). - Reply(http.StatusOK) - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{ - Running: true, - Health: &container.Health{Status: types.Healthy}, - }, - }}) - // Restarts services - utils.StorageId = "test-storage" - utils.GotrueId = "test-auth" - utils.RealtimeId = "test-realtime" - utils.PoolerId = "test-pooler" - utils.KongId = "test-kong" - for _, container := range listServicesToRestart() { - gock.New(utils.Docker.DaemonHost()). - Post("/v" + utils.Docker.ClientVersion() + "/containers/" + container + "/restart"). - Reply(http.StatusOK) - } - // Kong is excluded from the stack: no exec follows - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.KongId + "/json"). - Reply(http.StatusNotFound) - // Run test - err := RestartDatabase(context.Background(), io.Discard) - // Check error - assert.NoError(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on kong inspect failure", func(t *testing.T) { - utils.DbId = "test-reset" - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - // Restarts postgres - gock.New(utils.Docker.DaemonHost()). - Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/restart"). - Reply(http.StatusOK) - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{ - Running: true, - Health: &container.Health{Status: types.Healthy}, - }, - }}) - // Restarts services - utils.StorageId = "test-storage" - utils.GotrueId = "test-auth" - utils.RealtimeId = "test-realtime" - utils.PoolerId = "test-pooler" - utils.KongId = "test-kong" - for _, container := range listServicesToRestart() { - gock.New(utils.Docker.DaemonHost()). - Post("/v" + utils.Docker.ClientVersion() + "/containers/" + container + "/restart"). - Reply(http.StatusOK) - } - // A daemon error is not the excluded-kong skip case - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.KongId + "/json"). - Reply(http.StatusServiceUnavailable) - // Run test - err := RestartDatabase(context.Background(), io.Discard) - // Check error - assert.ErrorContains(t, err, "failed to inspect kong") - assert.Contains(t, utils.CmdSuggestion, "API routes may return 502") - assert.Contains(t, utils.CmdSuggestion, "docker restart test-kong") - t.Cleanup(func() { utils.CmdSuggestion = "" }) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on kong reload failure", func(t *testing.T) { - utils.DbId = "test-reset" - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - // Restarts postgres - gock.New(utils.Docker.DaemonHost()). - Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/restart"). - Reply(http.StatusOK) - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{ - Running: true, - Health: &container.Health{Status: types.Healthy}, - }, - }}) - // Restarts services - utils.StorageId = "test-storage" - utils.GotrueId = "test-auth" - utils.RealtimeId = "test-realtime" - utils.PoolerId = "test-pooler" - utils.KongId = "test-kong" - for _, container := range listServicesToRestart() { - gock.New(utils.Docker.DaemonHost()). - Post("/v" + utils.Docker.ClientVersion() + "/containers/" + container + "/restart"). - Reply(http.StatusOK) - } - // Kong is up but the reload exec fails - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.KongId + "/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{Running: true}, - }}) - gock.New(utils.Docker.DaemonHost()). - Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.KongId + "/exec"). - Reply(http.StatusServiceUnavailable) - // The reload must carry bring-up's template — see reloadKong (#6059). - execPath := "/v" + utils.Docker.ClientVersion() + "/containers/" + utils.KongId + "/exec" - observed := 0 - gock.Observe(func(r *http.Request, mock gock.Mock) { - if r.URL.Path != execPath { - return - } - observed += 1 - var body container.ExecOptions - assert.NoError(t, json.NewDecoder(r.Body).Decode(&body)) - assert.Equal(t, []string{"kong", "reload", "--nginx-conf", "/home/kong/custom_nginx.template"}, body.Cmd) - }) - t.Cleanup(func() { gock.Observe(nil) }) - // Run test - err := RestartDatabase(context.Background(), io.Discard) - // Check error - assert.Equal(t, 1, observed) - assert.ErrorContains(t, err, "failed to reload kong") - assert.Contains(t, utils.CmdSuggestion, "API routes may return 502") - assert.Contains(t, utils.CmdSuggestion, "docker restart test-kong") - t.Cleanup(func() { utils.CmdSuggestion = "" }) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on db restart failure", func(t *testing.T) { - utils.DbId = "test-reset" - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - // Restarts postgres - gock.New(utils.Docker.DaemonHost()). - Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/restart"). - Reply(http.StatusServiceUnavailable) - // Run test - err := RestartDatabase(context.Background(), io.Discard) - // Check error - assert.ErrorContains(t, err, "failed to restart container") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on health check timeout", func(t *testing.T) { - utils.DbId = "test-reset" - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Post("/v" + utils.Docker.ClientVersion() + "/containers/test-reset/restart"). - Reply(http.StatusOK) - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/test-reset/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{ - Running: false, - Status: "exited", - }, - }}) - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/test-reset/logs"). - Reply(http.StatusServiceUnavailable) - // Run test - err := RestartDatabase(context.Background(), io.Discard) - // Check error - assert.ErrorContains(t, err, "test-reset container is not running: exited") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} diff --git a/apps/cli-go/internal/db/start/start.go b/apps/cli-go/internal/db/start/start.go deleted file mode 100644 index d4c97ba1d5..0000000000 --- a/apps/cli-go/internal/db/start/start.go +++ /dev/null @@ -1,425 +0,0 @@ -package start - -import ( - "context" - _ "embed" - "fmt" - "io" - "os" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/cenkalti/backoff/v4" - "github.com/containerd/errdefs" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/network" - "github.com/docker/go-connections/nat" - "github.com/go-errors/errors" - "github.com/jackc/pgconn" - "github.com/jackc/pgx/v4" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/migration/apply" - "github.com/supabase/cli/internal/status" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/internal/utils/flags" - "github.com/supabase/cli/pkg/config" - "github.com/supabase/cli/pkg/migration" - "github.com/supabase/cli/pkg/vault" -) - -var ( - //go:embed templates/schema.sql - initialSchema string - //go:embed templates/webhook.sql - webhookSchema string - //go:embed templates/_supabase.sql - _supabaseSchema string - //go:embed templates/restore.sh - restoreScript string -) - -func Run(ctx context.Context, fromBackup string, fsys afero.Fs) error { - if err := flags.LoadConfig(fsys); err != nil { - return err - } - if err := utils.AssertSupabaseDbIsRunning(); err == nil { - fmt.Fprintln(os.Stderr, "Postgres database is already running.") - return nil - } else if !errors.Is(err, utils.ErrNotRunning) { - return err - } - err := StartDatabase(ctx, fromBackup, fsys, os.Stderr) - if err != nil { - if err := utils.DockerRemoveAll(context.Background(), os.Stderr, utils.Config.ProjectId); err != nil { - fmt.Fprintln(os.Stderr, err) - } - } - return err -} - -func NewContainerConfig(args ...string) container.Config { - env := []string{ - "POSTGRES_PASSWORD=" + utils.Config.Db.Password, - "POSTGRES_HOST=/var/run/postgresql", - "JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value, - fmt.Sprintf("JWT_EXP=%d", utils.Config.Auth.JwtExpiry), - } - if len(utils.Config.Db.OrioleDBVersion) > 0 { - env = append(env, - "POSTGRES_INITDB_ARGS=--lc-collate=C --lc-ctype=C", - fmt.Sprintf("S3_ENABLED=%t", true), - "S3_HOST="+utils.Config.Experimental.S3Host, - "S3_REGION="+utils.Config.Experimental.S3Region, - "S3_ACCESS_KEY="+utils.Config.Experimental.S3AccessKey, - "S3_SECRET_KEY="+utils.Config.Experimental.S3SecretKey, - ) - } else if i := strings.IndexByte(utils.Config.Db.Image, ':'); config.VersionCompare(utils.Config.Db.Image[i+1:], "15.8.1.005") < 0 { - env = append(env, "POSTGRES_INITDB_ARGS=--lc-collate=C.UTF-8") - } - config := container.Config{ - Image: utils.Config.Db.Image, - Env: env, - Healthcheck: &container.HealthConfig{ - Test: []string{"CMD", "pg_isready", "-U", "postgres", "-h", "127.0.0.1", "-p", "5432"}, - Interval: 10 * time.Second, - Timeout: 2 * time.Second, - Retries: 3, - }, - Entrypoint: []string{"sh", "-c", ` -cat <<'EOF' > /etc/postgresql.schema.sql && \ -cat <<'EOF' > /etc/postgresql-custom/pgsodium_root.key && \ -cat <<'EOF' >> /etc/postgresql/postgresql.conf && \ -docker-entrypoint.sh postgres -D /etc/postgresql ` + strings.Join(args, " ") + ` -` + initialSchema + ` -` + webhookSchema + ` -` + _supabaseSchema + ` -EOF -` + utils.Config.Db.RootKey.Value + ` -EOF -` + utils.Config.Db.Settings.ToPostgresConfig() + ` -EOF`}, - } - if utils.Config.Db.MajorVersion <= 14 { - config.Entrypoint = []string{"sh", "-c", ` -cat <<'EOF' > /docker-entrypoint-initdb.d/supabase_schema.sql && \ -cat <<'EOF' >> /etc/postgresql/postgresql.conf && \ -docker-entrypoint.sh postgres -D /etc/postgresql ` + strings.Join(args, " ") + ` -` + _supabaseSchema + ` -EOF -` + utils.Config.Db.Settings.ToPostgresConfig() + ` -EOF`} - } - return config -} - -func NewHostConfig() container.HostConfig { - hostPort := strconv.FormatUint(uint64(utils.Config.Db.Port), 10) - hostConfig := container.HostConfig{ - PortBindings: nat.PortMap{"5432/tcp": []nat.PortBinding{{HostPort: hostPort}}}, - RestartPolicy: container.RestartPolicy{Name: container.RestartPolicyUnlessStopped}, - Binds: []string{ - utils.DbId + ":/var/lib/postgresql/data", - }, - } - if utils.Config.Db.MajorVersion <= 14 { - hostConfig.Tmpfs = map[string]string{"/docker-entrypoint-initdb.d": ""} - } - return hostConfig -} - -func StartDatabase(ctx context.Context, fromBackup string, fsys afero.Fs, w io.Writer, options ...func(*pgx.ConnConfig)) error { - config := NewContainerConfig() - hostConfig := NewHostConfig() - networkingConfig := network.NetworkingConfig{ - EndpointsConfig: map[string]*network.EndpointSettings{ - utils.NetId: { - Aliases: utils.DbAliases, - }, - }, - } - if len(fromBackup) > 0 { - config.Entrypoint = []string{"sh", "-c", ` -cat <<'EOF' > /etc/postgresql.schema.sql && \ -cat <<'EOF' > /docker-entrypoint-initdb.d/migrate.sh && \ -cat <<'EOF' > /etc/postgresql-custom/pgsodium_root.key && \ -cat <<'EOF' >> /etc/postgresql/postgresql.conf && \ -docker-entrypoint.sh postgres -D /etc/postgresql -` + initialSchema + ` -` + _supabaseSchema + ` -EOF -` + restoreScript + ` -EOF -` + utils.Config.Db.RootKey.Value + ` -EOF -` + utils.Config.Db.Settings.ToPostgresConfig() + ` -cron.launch_active_jobs = off -EOF`} - if !filepath.IsAbs(fromBackup) { - fromBackup = filepath.Join(utils.CurrentDirAbs, fromBackup) - } - hostConfig.Binds = append(hostConfig.Binds, utils.ToDockerPath(fromBackup)+":/etc/backup.sql:ro") - } - // Creating volume will not override existing volume, so we must inspect explicitly - _, err := utils.Docker.VolumeInspect(ctx, utils.DbId) - utils.NoBackupVolume = errdefs.IsNotFound(err) - if utils.NoBackupVolume { - fmt.Fprintln(w, "Starting database...") - } else if len(fromBackup) > 0 { - utils.CmdSuggestion = fmt.Sprintf("Run %s to remove existing docker volumes.", utils.Aqua("supabase stop --no-backup")) - return errors.Errorf("backup volume already exists") - } else { - fmt.Fprintln(w, "Starting database from backup...") - } - if _, err := utils.DockerStart(ctx, config, hostConfig, networkingConfig, utils.DbId); err != nil { - return err - } - // Ignore health check because restoring a large backup may take longer than 2 minutes - if err := WaitForHealthyService(ctx, utils.Config.Db.HealthTimeout, utils.DbId); err != nil && len(fromBackup) == 0 { - return err - } - // Initialize if we are on PG14 and there's no existing db volume - if utils.NoBackupVolume && len(fromBackup) == 0 { - if err := SetupLocalDatabase(ctx, "", fsys, w, options...); err != nil { - return err - } - } - return initCurrentBranch(fsys) -} - -func NewBackoffPolicy(ctx context.Context, timeout time.Duration) backoff.BackOff { - policy := backoff.WithMaxRetries( - backoff.NewConstantBackOff(time.Second), - uint64(timeout.Seconds()), - ) - return backoff.WithContext(policy, ctx) -} - -func WaitForHealthyService(ctx context.Context, timeout time.Duration, started ...string) error { - probe := func() error { - var errHealth []error - var unhealthy []string - for _, container := range started { - if err := status.IsServiceReady(ctx, container); err != nil { - unhealthy = append(unhealthy, container) - errHealth = append(errHealth, err) - } - } - started = unhealthy - return errors.Join(errHealth...) - } - policy := NewBackoffPolicy(ctx, timeout) - err := backoff.Retry(probe, policy) - if err != nil && !errors.Is(err, context.Canceled) { - // Print container logs for easier debugging - for _, containerId := range started { - fmt.Fprintln(os.Stderr, containerId, "container logs:") - if err := utils.DockerStreamLogsOnce(context.Background(), containerId, os.Stderr, os.Stderr); err != nil { - fmt.Fprintln(os.Stderr, err) - } - } - } - return err -} - -func IsUnhealthyError(err error) bool { - // Health check always returns a joinError - _, ok := err.(interface{ Unwrap() []error }) - return ok -} - -func initCurrentBranch(fsys afero.Fs) error { - // Create _current_branch file to avoid breaking db branch commands - if _, err := fsys.Stat(utils.CurrBranchPath); err == nil { - return nil - } else if !errors.Is(err, os.ErrNotExist) { - return errors.Errorf("failed init current branch: %w", err) - } - return utils.WriteFile(utils.CurrBranchPath, []byte("main"), fsys) -} - -func initSchema(ctx context.Context, conn *pgx.Conn, host string, w io.Writer) error { - fmt.Fprintln(w, "Initialising schema...") - if utils.Config.Db.MajorVersion <= 14 { - if file, err := migration.NewMigrationFromReader(strings.NewReader(utils.GlobalsSql)); err != nil { - return err - } else if err := file.ExecBatch(ctx, conn); err != nil { - return err - } - return InitSchema14(ctx, conn) - } - return initSchema15(ctx, host) -} - -func InitSchema14(ctx context.Context, conn *pgx.Conn) error { - sql := utils.InitialSchemaPg14Sql - if utils.Config.Db.MajorVersion == 13 { - sql = utils.InitialSchemaPg13Sql - } - file, err := migration.NewMigrationFromReader(strings.NewReader(sql)) - if err != nil { - return err - } - return file.ExecBatch(ctx, conn) -} - -func initRealtimeJob(host, jwks string) utils.DockerJob { - return utils.DockerJob{ - Image: utils.Config.Realtime.Image, - Env: []string{ - "PORT=4000", - "DB_HOST=" + host, - "DB_PORT=5432", - "DB_USER=" + utils.SUPERUSER_ROLE, - "DB_PASSWORD=" + utils.Config.Db.Password, - "DB_NAME=postgres", - "DB_AFTER_CONNECT_QUERY=SET search_path TO _realtime", - "DB_ENC_KEY=" + utils.Config.Realtime.EncryptionKey, - fmt.Sprintf("API_JWT_JWKS=%s", jwks), - "API_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value, - "METRICS_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value, - "APP_NAME=realtime", - "SECRET_KEY_BASE=" + utils.Config.Realtime.SecretKeyBase, - "ERL_AFLAGS=" + utils.ToRealtimeEnv(utils.Config.Realtime.IpVersion), - "DNS_NODES=''", - "RLIMIT_NOFILE=", - "SEED_SELF_HOST=true", - "RUN_JANITOR=true", - fmt.Sprintf("MAX_HEADER_LENGTH=%d", utils.Config.Realtime.MaxHeaderLength), - }, - Cmd: []string{"/app/bin/realtime", "eval", fmt.Sprintf(`{:ok, _} = Application.ensure_all_started(:realtime) -{:ok, _} = Realtime.Tenants.health_check("%s")`, utils.Config.Realtime.TenantId)}, - } -} - -func initStorageJob(host string) utils.DockerJob { - return utils.DockerJob{ - Image: utils.Config.Storage.Image, - Env: []string{ - "DB_INSTALL_ROLES=false", - "DB_MIGRATIONS_FREEZE_AT=" + utils.Config.Storage.TargetMigration, - "ANON_KEY=" + utils.Config.Auth.AnonKey.Value, - "SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey.Value, - "PGRST_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value, - fmt.Sprintf("DATABASE_URL=postgresql://supabase_storage_admin:%s@%s:5432/postgres", utils.Config.Db.Password, host), - fmt.Sprintf("FILE_SIZE_LIMIT=%v", utils.Config.Storage.FileSizeLimit), - "STORAGE_BACKEND=file", - "STORAGE_FILE_BACKEND_PATH=/mnt", - "TENANT_ID=stub", - // TODO: https://github.com/supabase/storage-api/issues/55 - "REGION=stub", - "GLOBAL_S3_BUCKET=stub", - }, - Cmd: []string{"node", "dist/scripts/migrate-call.js"}, - } -} - -func initAuthJob(host string) utils.DockerJob { - return utils.DockerJob{ - Image: utils.Config.Auth.Image, - Env: []string{ - "API_EXTERNAL_URL=" + utils.Config.AuthExternalURL(), - "GOTRUE_LOG_LEVEL=error", - "GOTRUE_DB_DRIVER=postgres", - fmt.Sprintf("GOTRUE_DB_DATABASE_URL=postgresql://supabase_auth_admin:%s@%s:5432/postgres", utils.Config.Db.Password, host), - "GOTRUE_SITE_URL=" + utils.Config.Auth.SiteUrl, - "GOTRUE_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value, - }, - Cmd: []string{"gotrue", "migrate"}, - } -} - -func initSchema15(ctx context.Context, host string) error { - // Apply service migrations - var initJobs []utils.DockerJob - if utils.Config.Realtime.Enabled { - jwks, err := utils.Config.Auth.ResolveJWKS(context.Background()) - if err != nil { - return err - } - initJobs = append(initJobs, initRealtimeJob(host, jwks)) - } - if utils.Config.Storage.Enabled { - initJobs = append(initJobs, initStorageJob(host)) - } - if utils.Config.Auth.Enabled { - initJobs = append(initJobs, initAuthJob(host)) - } - logger := utils.GetDebugLogger() - for _, job := range initJobs { - if err := utils.DockerRunJob(ctx, job, io.Discard, logger); err != nil { - return err - } - } - return nil -} - -func SetupLocalDatabase(ctx context.Context, version string, fsys afero.Fs, w io.Writer, options ...func(*pgx.ConnConfig)) error { - conn, err := utils.ConnectLocalPostgres(ctx, pgconn.Config{}, options...) - if err != nil { - return err - } - defer conn.Close(context.Background()) - if err := SetupDatabase(ctx, conn, utils.DbId, w, fsys); err != nil { - return err - } - if err := apply.MigrateAndSeed(ctx, version, conn, fsys); err != nil { - return err - } - return nil -} - -func SetupDatabase(ctx context.Context, conn *pgx.Conn, host string, w io.Writer, fsys afero.Fs) error { - if err := initSchema(ctx, conn, host, w); err != nil { - return err - } - if err := ApplyApiPrivileges(ctx, conn); err != nil { - return err - } - // Create vault secrets first so roles.sql can reference them - if err := vault.UpsertVaultSecrets(ctx, utils.Config.Db.Vault, conn); err != nil { - return err - } - err := migration.SeedGlobals(ctx, []string{utils.CustomRolesPath}, conn, afero.NewIOFS(fsys)) - if errors.Is(err, os.ErrNotExist) { - return nil - } - return err -} - -// RevokeDefaultDataApiPrivilegesSql matches the SQL that Studio runs at cloud project creation -// when the "Default privileges for new entities" toggle is off. It removes the default GRANTs -// applied by the initial schema so newly-created entities in `public` owned by `postgres` are -// not exposed through the Data API roles until explicit GRANTs are issued. -const RevokeDefaultDataApiPrivilegesSql = ` -alter default privileges for role postgres in schema public - revoke select, insert, update, delete on tables from anon, authenticated, service_role; -alter default privileges for role postgres in schema public - revoke usage, select on sequences from anon, authenticated, service_role; -alter default privileges for role postgres in schema public - revoke execute on functions from anon, authenticated, service_role; -` - -// ApplyApiPrivileges adjusts the default privileges on the `public` schema to match the -// `[api].auto_expose_new_tables` flag in config.toml. The flag is tri-state to give users a -// safe migration window: -// -// - unset (default): revoke the default Data API GRANTs so newly-created entities in `public` -// require explicit GRANTs to surface through the Data API, matching the new cloud default. -// The implicit default flipped to false on May 30, 2026; the flag is removed entirely in -// October 2026 (always-revoked behaviour). -// - true: explicit opt-in to the legacy behaviour of keeping the bundled initial-schema GRANTs -// in place. Deprecated: config loading warns that the flag is removed on October 30, 2026. -// - false: revoke the default Data API GRANTs (same as unset). Kept so users can pin the new -// behaviour explicitly during the deprecation window. -func ApplyApiPrivileges(ctx context.Context, conn *pgx.Conn) error { - if utils.Config.Api.AutoExposeNewTables != nil && *utils.Config.Api.AutoExposeNewTables { - return nil - } - file, err := migration.NewMigrationFromReader(strings.NewReader(RevokeDefaultDataApiPrivilegesSql)) - if err != nil { - return err - } - return file.ExecBatch(ctx, conn) -} diff --git a/apps/cli-go/internal/db/start/start_test.go b/apps/cli-go/internal/db/start/start_test.go deleted file mode 100644 index 805691b8ec..0000000000 --- a/apps/cli-go/internal/db/start/start_test.go +++ /dev/null @@ -1,434 +0,0 @@ -package start - -import ( - "context" - "errors" - "io" - "net/http" - "os" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/volume" - "github.com/h2non/gock" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/testing/apitest" - "github.com/supabase/cli/internal/testing/fstest" - "github.com/supabase/cli/internal/testing/helper" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/cast" - "github.com/supabase/cli/pkg/pgtest" -) - -func TestInitBranch(t *testing.T) { - t.Run("throws error on permission denied", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewReadOnlyFs(afero.NewMemMapFs()) - // Run test - err := initCurrentBranch(fsys) - // Check error - assert.ErrorContains(t, err, "operation not permitted") - }) - - t.Run("throws error on stat failure", func(t *testing.T) { - // Setup in-memory fs - fsys := &fstest.StatErrorFs{DenyPath: utils.CurrBranchPath} - // Run test - err := initCurrentBranch(fsys) - // Check error - assert.ErrorContains(t, err, "permission denied") - }) - - t.Run("throws error on write failure", func(t *testing.T) { - // Setup in-memory fs - fsys := &fstest.OpenErrorFs{DenyPath: utils.CurrBranchPath} - // Run test - err := initCurrentBranch(fsys) - // Check error - assert.ErrorContains(t, err, "permission denied") - }) -} - -func TestStartDatabase(t *testing.T) { - t.Run("initialize main branch", func(t *testing.T) { - utils.Config.Db.MajorVersion = 15 - utils.DbId = "supabase_db_test" - utils.Config.Db.Port = 5432 - // Setup in-memory fs - fsys := afero.NewMemMapFs() - roles := "create role test" - require.NoError(t, afero.WriteFile(fsys, utils.CustomRolesPath, []byte(roles), 0644)) - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/volumes/" + utils.DbId). - Reply(http.StatusNotFound). - JSON(volume.Volume{}) - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), utils.DbId) - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{ - Running: true, - Health: &container.Health{Status: types.Healthy}, - }, - }}) - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Realtime.Image), "test-realtime") - require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-realtime", "")) - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Storage.Image), "test-storage") - require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-storage", "")) - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Auth.Image), "test-auth") - require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-auth", "")) - // Setup mock postgres: with auto_expose_new_tables unset, the implicit default revokes - // the default Data API GRANTs before seeding roles. - conn := pgtest.NewConn() - defer conn.Close(t) - helper.MockApiPrivilegesRevoke(conn). - Query(roles). - Reply("CREATE ROLE") - // Run test - err := StartDatabase(context.Background(), "", fsys, io.Discard, conn.Intercept) - // Check error - assert.NoError(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) - // Check current branch - contents, err := afero.ReadFile(fsys, utils.CurrBranchPath) - assert.NoError(t, err) - assert.Equal(t, []byte("main"), contents) - }) - - t.Run("recover from backup volume", func(t *testing.T) { - utils.Config.Db.MajorVersion = 14 - utils.DbId = "supabase_db_test" - utils.Config.Db.Port = 5432 - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/volumes/" + utils.DbId). - Reply(http.StatusOK). - JSON(volume.Volume{}) - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), utils.DbId) - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{ - Running: true, - Health: &container.Health{Status: types.Healthy}, - }, - }}) - // Run test - err := StartDatabase(context.Background(), "", fsys, io.Discard) - // Check error - assert.NoError(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) - // Check current branch - contents, err := afero.ReadFile(fsys, utils.CurrBranchPath) - assert.NoError(t, err) - assert.Equal(t, []byte("main"), contents) - }) - - t.Run("throws error on start failure", func(t *testing.T) { - utils.Config.Db.MajorVersion = 15 - utils.DbId = "supabase_db_test" - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/volumes/" + utils.DbId). - ReplyError(errors.New("network error")) - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Config.Db.Image) + "/json"). - Reply(http.StatusServiceUnavailable) - // Run test - err := StartDatabase(context.Background(), "", fsys, io.Discard) - // Check error - assert.ErrorContains(t, err, "request returned 503 Service Unavailable for API route and version") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} - -func TestStartCommand(t *testing.T) { - t.Run("throws error on malformed config", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, afero.WriteFile(fsys, utils.ConfigPath, []byte("malformed"), 0644)) - // Run test - err := Run(context.Background(), "", fsys) - // Check error - assert.ErrorContains(t, err, "toml: expected = after a key, but the document ends there") - }) - - t.Run("throws error on missing docker", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers"). - ReplyError(errors.New("network error")) - // Run test - err := Run(context.Background(), "", fsys) - // Check error - assert.ErrorContains(t, err, "network error") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("exits if already started", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/"). - Reply(http.StatusOK). - JSON(container.InspectResponse{}) - // Run test - err := Run(context.Background(), "", fsys) - // Check error - assert.NoError(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on start failure", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/"). - Reply(http.StatusNotFound) - // Fail to start - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/volumes/"). - ReplyError(errors.New("network error")) - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Config.Db.Image) + "/json"). - ReplyError(errors.New("network error")) - // Cleanup resources - apitest.MockDockerStop(utils.Docker) - // Run test - err := Run(context.Background(), "", fsys) - // Check error - assert.ErrorContains(t, err, "network error") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} - -func TestSetupDatabase(t *testing.T) { - utils.Config.Db.MajorVersion = 15 - - t.Run("revokes default data api privileges when auto_expose_new_tables is unset", func(t *testing.T) { - utils.Config.Db.MajorVersion = 14 - utils.Config.Api.AutoExposeNewTables = nil - defer func() { - utils.Config.Db.MajorVersion = 15 - }() - utils.Config.Db.Port = 5432 - utils.GlobalsSql = "create schema public" - utils.InitialSchemaPg14Sql = "create schema private" - // Setup in-memory fs - fsys := afero.NewMemMapFs() - roles := "create role postgres" - require.NoError(t, afero.WriteFile(fsys, utils.CustomRolesPath, []byte(roles), 0644)) - // Setup mock postgres: with the flag unset, the implicit default now revokes the - // default Data API GRANTs (the May 30 2026 flip) between initial schema and roles.sql - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(utils.GlobalsSql). - Reply("CREATE SCHEMA"). - Query(utils.InitialSchemaPg14Sql). - Reply("CREATE SCHEMA") - helper.MockApiPrivilegesRevoke(conn). - Query(roles). - Reply("CREATE ROLE") - // Run test - err := SetupLocalDatabase(context.Background(), "", fsys, io.Discard, conn.Intercept) - // Check error - assert.NoError(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("keeps default data api privileges when auto_expose_new_tables is true", func(t *testing.T) { - utils.Config.Db.MajorVersion = 14 - flag := true - utils.Config.Api.AutoExposeNewTables = &flag - defer func() { - utils.Config.Db.MajorVersion = 15 - utils.Config.Api.AutoExposeNewTables = nil - }() - utils.Config.Db.Port = 5432 - utils.GlobalsSql = "create schema public" - utils.InitialSchemaPg14Sql = "create schema private" - // Setup in-memory fs - fsys := afero.NewMemMapFs() - roles := "create role postgres" - require.NoError(t, afero.WriteFile(fsys, utils.CustomRolesPath, []byte(roles), 0644)) - // Setup mock postgres: explicit true opts into the legacy behaviour, so no revoke SQL - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(utils.GlobalsSql). - Reply("CREATE SCHEMA"). - Query(utils.InitialSchemaPg14Sql). - Reply("CREATE SCHEMA"). - Query(roles). - Reply("CREATE ROLE") - // Run test - err := SetupLocalDatabase(context.Background(), "", fsys, io.Discard, conn.Intercept) - // Check error - assert.NoError(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("revokes default data api privileges when auto_expose_new_tables is false", func(t *testing.T) { - utils.Config.Db.MajorVersion = 14 - flag := false - utils.Config.Api.AutoExposeNewTables = &flag - defer func() { - utils.Config.Db.MajorVersion = 15 - utils.Config.Api.AutoExposeNewTables = nil - }() - utils.Config.Db.Port = 5432 - utils.GlobalsSql = "create schema public" - utils.InitialSchemaPg14Sql = "create schema private" - // Setup in-memory fs - fsys := afero.NewMemMapFs() - roles := "create role postgres" - require.NoError(t, afero.WriteFile(fsys, utils.CustomRolesPath, []byte(roles), 0644)) - // Setup mock postgres: the revoke SQL must execute between the initial schema and roles.sql - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(utils.GlobalsSql). - Reply("CREATE SCHEMA"). - Query(utils.InitialSchemaPg14Sql). - Reply("CREATE SCHEMA") - helper.MockApiPrivilegesRevoke(conn). - Query(roles). - Reply("CREATE ROLE") - // Run test - err := SetupLocalDatabase(context.Background(), "", fsys, io.Discard, conn.Intercept) - // Check error - assert.NoError(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on connect failure", func(t *testing.T) { - utils.Config.Db.Port = 0 - // Run test - err := SetupLocalDatabase(context.Background(), "", nil, io.Discard) - // Check error - assert.ErrorContains(t, err, "invalid port (outside range)") - }) - - t.Run("throws error on init failure", func(t *testing.T) { - utils.Config.Realtime.Enabled = true - utils.Config.Db.Port = 5432 - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Config.Realtime.Image) + "/json"). - ReplyError(errors.New("network error")) - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - // Run test - err := SetupLocalDatabase(context.Background(), "", nil, io.Discard, conn.Intercept) - // Check error - assert.ErrorContains(t, err, "network error") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on read failure", func(t *testing.T) { - utils.Config.Db.Port = 5432 - // Setup in-memory fs - fsys := &fstest.OpenErrorFs{DenyPath: utils.CustomRolesPath} - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Realtime.Image), "test-realtime") - require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-realtime", "")) - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Storage.Image), "test-storage") - require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-storage", "")) - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Auth.Image), "test-auth") - require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-auth", "")) - // Setup mock postgres: the default-privilege revoke runs before roles.sql is read - conn := pgtest.NewConn() - defer conn.Close(t) - helper.MockApiPrivilegesRevoke(conn) - // Run test - err := SetupLocalDatabase(context.Background(), "", fsys, io.Discard, conn.Intercept) - // Check error - assert.ErrorIs(t, err, os.ErrPermission) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} -func TestStartDatabaseWithCustomSettings(t *testing.T) { - t.Run("starts database with custom MaxConnections", func(t *testing.T) { - // Setup - utils.Config.Db.MajorVersion = 15 - utils.DbId = "supabase_db_test" - utils.Config.Db.Port = 5432 - utils.Config.Db.Settings.MaxConnections = cast.Ptr(uint(50)) - - // Setup in-memory fs - fsys := afero.NewMemMapFs() - - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/volumes/" + utils.DbId). - Reply(http.StatusNotFound). - JSON(volume.Volume{}) - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), utils.DbId) - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{ - Running: true, - Health: &container.Health{Status: types.Healthy}, - }, - }}) - - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Realtime.Image), "test-realtime") - require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-realtime", "")) - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Storage.Image), "test-storage") - require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-storage", "")) - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Auth.Image), "test-auth") - require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-auth", "")) - // Setup mock postgres: with auto_expose_new_tables unset, the default Data API GRANTs - // are revoked by default. - conn := pgtest.NewConn() - defer conn.Close(t) - helper.MockApiPrivilegesRevoke(conn) - - // Run test - err := StartDatabase(context.Background(), "", fsys, io.Discard, conn.Intercept) - - // Check error - assert.NoError(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) - - // Check if the custom MaxConnections setting was applied - config := NewContainerConfig() - assert.Contains(t, config.Entrypoint[2], "max_connections = 50") - }) -} diff --git a/apps/cli-go/internal/db/start/templates/_supabase.sql b/apps/cli-go/internal/db/start/templates/_supabase.sql deleted file mode 100644 index 6e5d8487ba..0000000000 --- a/apps/cli-go/internal/db/start/templates/_supabase.sql +++ /dev/null @@ -1,13 +0,0 @@ -CREATE DATABASE _supabase WITH OWNER postgres; - --- Switch to the newly created _supabase database -\c _supabase --- Create schemas in _supabase database for --- internals tools and reports to not overload user database --- with non-user activity -CREATE SCHEMA IF NOT EXISTS _analytics; -ALTER SCHEMA _analytics OWNER TO postgres; - -CREATE SCHEMA IF NOT EXISTS _supavisor; -ALTER SCHEMA _supavisor OWNER TO postgres; -\c postgres diff --git a/apps/cli-go/internal/db/start/templates/restore.sh b/apps/cli-go/internal/db/start/templates/restore.sh deleted file mode 100755 index 3a1729ad97..0000000000 --- a/apps/cli-go/internal/db/start/templates/restore.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/sh -set -eu - -####################################### -# Used by both ami and docker builds to initialise database schema. -# Env vars: -# POSTGRES_DB defaults to postgres -# POSTGRES_HOST defaults to localhost -# POSTGRES_PORT defaults to 5432 -# POSTGRES_PASSWORD defaults to "" -# USE_DBMATE defaults to "" -# Exit code: -# 0 if migration succeeds, non-zero on error. -####################################### - -export PGDATABASE="${POSTGRES_DB:-postgres}" -export PGHOST="${POSTGRES_HOST:-localhost}" -export PGPORT="${POSTGRES_PORT:-5432}" -export PGPASSWORD="${POSTGRES_PASSWORD:-}" - -echo "$0: restoring roles" -cat "/etc/backup.sql" \ -| grep 'CREATE ROLE' \ -| grep -v 'supabase_admin' \ -| sed -E 's/^(CREATE ROLE postgres);/\1 WITH SUPERUSER;/' \ -| psql -v ON_ERROR_STOP=1 --no-password --no-psqlrc -U supabase_admin - -echo "$0: restoring schema" -cat "/etc/backup.sql" \ -| sed -E 's/^\\(un)?restrict .*$/-- &/' \ -| sed -E 's/^CREATE VIEW /CREATE OR REPLACE VIEW /' \ -| sed -E 's/^CREATE FUNCTION /CREATE OR REPLACE FUNCTION /' \ -| sed -E 's/^CREATE TRIGGER /CREATE OR REPLACE TRIGGER /' \ -| sed -E 's/^GRANT ALL ON FUNCTION graphql_public\./-- &/' \ -| sed -E 's/^CREATE ROLE /-- &/' \ -| sed -e '/ALTER ROLE postgres WITH / { h; $p; d; }' -e '$G' \ -| psql -v ON_ERROR_STOP=1 --no-password --no-psqlrc -U supabase_admin - -# run any post migration script to update role passwords -postinit="/etc/postgresql.schema.sql" -if [ -e "$postinit" ]; then - echo "$0: running $postinit" - psql -v ON_ERROR_STOP=1 --no-password --no-psqlrc -U supabase_admin -f "$postinit" -fi diff --git a/apps/cli-go/internal/db/start/templates/schema.sql b/apps/cli-go/internal/db/start/templates/schema.sql deleted file mode 100644 index dee166ae58..0000000000 --- a/apps/cli-go/internal/db/start/templates/schema.sql +++ /dev/null @@ -1,17 +0,0 @@ -\set pgpass `echo "$PGPASSWORD"` -\set jwt_secret `echo "$JWT_SECRET"` -\set jwt_exp `echo "$JWT_EXP"` - -ALTER DATABASE postgres SET "app.settings.jwt_secret" TO :'jwt_secret'; -ALTER DATABASE postgres SET "app.settings.jwt_exp" TO :'jwt_exp'; - -ALTER USER postgres WITH PASSWORD :'pgpass'; -ALTER USER authenticator WITH PASSWORD :'pgpass'; -ALTER USER pgbouncer WITH PASSWORD :'pgpass'; -ALTER USER supabase_auth_admin WITH PASSWORD :'pgpass'; -ALTER USER supabase_storage_admin WITH PASSWORD :'pgpass'; -ALTER USER supabase_replication_admin WITH PASSWORD :'pgpass'; -ALTER USER supabase_read_only_user WITH PASSWORD :'pgpass'; - -create schema if not exists _realtime; -alter schema _realtime owner to postgres; diff --git a/apps/cli-go/internal/db/start/templates/webhook.sql b/apps/cli-go/internal/db/start/templates/webhook.sql deleted file mode 100644 index 52cd097473..0000000000 --- a/apps/cli-go/internal/db/start/templates/webhook.sql +++ /dev/null @@ -1,232 +0,0 @@ -BEGIN; - --- Create pg_net extension -CREATE EXTENSION IF NOT EXISTS pg_net SCHEMA extensions; - --- Create supabase_functions schema -CREATE SCHEMA supabase_functions AUTHORIZATION supabase_admin; - -GRANT USAGE ON SCHEMA supabase_functions TO postgres, anon, authenticated, service_role; -ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON TABLES TO postgres, anon, authenticated, service_role; -ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO postgres, anon, authenticated, service_role; -ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO postgres, anon, authenticated, service_role; - --- supabase_functions.migrations definition -CREATE TABLE supabase_functions.migrations ( - version text PRIMARY KEY, - inserted_at timestamptz NOT NULL DEFAULT NOW() -); - --- Initial supabase_functions migration -INSERT INTO supabase_functions.migrations (version) VALUES ('initial'); - --- supabase_functions.hooks definition -CREATE TABLE supabase_functions.hooks ( - id bigserial PRIMARY KEY, - hook_table_id integer NOT NULL, - hook_name text NOT NULL, - created_at timestamptz NOT NULL DEFAULT NOW(), - request_id bigint -); -CREATE INDEX supabase_functions_hooks_request_id_idx ON supabase_functions.hooks USING btree (request_id); -CREATE INDEX supabase_functions_hooks_h_table_id_h_name_idx ON supabase_functions.hooks USING btree (hook_table_id, hook_name); -COMMENT ON TABLE supabase_functions.hooks IS 'Supabase Functions Hooks: Audit trail for triggered hooks.'; - -CREATE FUNCTION supabase_functions.http_request() - RETURNS trigger - LANGUAGE plpgsql - AS $function$ - DECLARE - request_id bigint; - payload jsonb; - url text := TG_ARGV[0]::text; - method text := TG_ARGV[1]::text; - headers jsonb DEFAULT '{}'::jsonb; - params jsonb DEFAULT '{}'::jsonb; - timeout_ms integer DEFAULT 1000; - BEGIN - IF url IS NULL OR url = 'null' THEN - RAISE EXCEPTION 'url argument is missing'; - END IF; - - IF method IS NULL OR method = 'null' THEN - RAISE EXCEPTION 'method argument is missing'; - END IF; - - IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN - headers = '{"Content-Type": "application/json"}'::jsonb; - ELSE - headers = TG_ARGV[2]::jsonb; - END IF; - - IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN - params = '{}'::jsonb; - ELSE - params = TG_ARGV[3]::jsonb; - END IF; - - IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN - timeout_ms = 1000; - ELSE - timeout_ms = TG_ARGV[4]::integer; - END IF; - - CASE - WHEN method = 'GET' THEN - SELECT http_get INTO request_id FROM net.http_get( - url, - params, - headers, - timeout_ms - ); - WHEN method = 'POST' THEN - payload = jsonb_build_object( - 'old_record', OLD, - 'record', NEW, - 'type', TG_OP, - 'table', TG_TABLE_NAME, - 'schema', TG_TABLE_SCHEMA - ); - - SELECT http_post INTO request_id FROM net.http_post( - url, - payload, - params, - headers, - timeout_ms - ); - ELSE - RAISE EXCEPTION 'method argument % is invalid', method; - END CASE; - - INSERT INTO supabase_functions.hooks - (hook_table_id, hook_name, request_id) - VALUES - (TG_RELID, TG_NAME, request_id); - - RETURN NEW; - END -$function$; - --- Supabase super admin -DO -$$ -BEGIN - IF NOT EXISTS ( - SELECT 1 - FROM pg_roles - WHERE rolname = 'supabase_functions_admin' - ) - THEN - CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION; - END IF; -END -$$; - -GRANT ALL PRIVILEGES ON SCHEMA supabase_functions TO supabase_functions_admin; -GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA supabase_functions TO supabase_functions_admin; -GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA supabase_functions TO supabase_functions_admin; -ALTER USER supabase_functions_admin SET search_path = "supabase_functions"; -ALTER table "supabase_functions".migrations OWNER TO supabase_functions_admin; -ALTER table "supabase_functions".hooks OWNER TO supabase_functions_admin; -ALTER function "supabase_functions".http_request() OWNER TO supabase_functions_admin; -GRANT supabase_functions_admin TO postgres; - --- Remove unused supabase_pg_net_admin role -DO -$$ -BEGIN - IF EXISTS ( - SELECT 1 - FROM pg_roles - WHERE rolname = 'supabase_pg_net_admin' - ) - THEN - REASSIGN OWNED BY supabase_pg_net_admin TO supabase_admin; - DROP OWNED BY supabase_pg_net_admin; - DROP ROLE supabase_pg_net_admin; - END IF; -END -$$; - --- pg_net grants when extension is already enabled -DO -$$ -BEGIN - IF EXISTS ( - SELECT 1 - FROM pg_extension - WHERE extname = 'pg_net' - ) - THEN - GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role; - - ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; - ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; - - ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; - ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; - - REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; - REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; - - GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; - GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; - END IF; -END -$$; - --- Event trigger for pg_net -CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access() -RETURNS event_trigger -LANGUAGE plpgsql -AS $$ -BEGIN - IF EXISTS ( - SELECT 1 - FROM pg_event_trigger_ddl_commands() AS ev - JOIN pg_extension AS ext - ON ev.objid = ext.oid - WHERE ext.extname = 'pg_net' - ) - THEN - GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role; - - ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; - ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; - - ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; - ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; - - REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; - REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; - - GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; - GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; - END IF; -END; -$$; -COMMENT ON FUNCTION extensions.grant_pg_net_access IS 'Grants access to pg_net'; - -DO -$$ -BEGIN - IF NOT EXISTS ( - SELECT 1 - FROM pg_event_trigger - WHERE evtname = 'issue_pg_net_access' - ) THEN - CREATE EVENT TRIGGER issue_pg_net_access ON ddl_command_end WHEN TAG IN ('CREATE EXTENSION') - EXECUTE PROCEDURE extensions.grant_pg_net_access(); - END IF; -END -$$; - -INSERT INTO supabase_functions.migrations (version) VALUES ('20210809183423_update_grants'); - -ALTER function supabase_functions.http_request() SECURITY DEFINER; -ALTER function supabase_functions.http_request() SET search_path = supabase_functions; -REVOKE ALL ON FUNCTION supabase_functions.http_request() FROM PUBLIC; -GRANT EXECUTE ON FUNCTION supabase_functions.http_request() TO postgres, anon, authenticated, service_role; - -COMMIT; diff --git a/apps/cli-go/internal/debug/http.go b/apps/cli-go/internal/debug/http.go deleted file mode 100644 index b22f6bbc8a..0000000000 --- a/apps/cli-go/internal/debug/http.go +++ /dev/null @@ -1,24 +0,0 @@ -package debug - -import ( - "log" - "net/http" - "os" -) - -type debugTransport struct { - http.RoundTripper - logger *log.Logger -} - -func (t *debugTransport) RoundTrip(req *http.Request) (*http.Response, error) { - t.logger.Printf("%s: %s\n", req.Method, req.URL) - return t.RoundTripper.RoundTrip(req) -} - -func NewTransport() http.RoundTripper { - return &debugTransport{ - http.DefaultTransport, - log.New(os.Stderr, "HTTP ", log.LstdFlags|log.Lmsgprefix), - } -} diff --git a/apps/cli-go/internal/debug/postgres.go b/apps/cli-go/internal/debug/postgres.go deleted file mode 100644 index 3d774cb309..0000000000 --- a/apps/cli-go/internal/debug/postgres.go +++ /dev/null @@ -1,262 +0,0 @@ -package debug - -import ( - "context" - "crypto/tls" - "encoding/binary" - "encoding/json" - "errors" - "io" - "log" - "net" - "os" - "time" - - "github.com/jackc/pgproto3/v2" - "github.com/jackc/pgx/v4" - "google.golang.org/grpc/test/bufconn" -) - -type Proxy struct { - dialContext func(ctx context.Context, network, addr string) (net.Conn, error) - // tlsPlan is pgconn's TLS config per attempt, primary first. DialFunc walks it - // so sslmode=allow keeps its plaintext-then-TLS retry, and completes the - // handshake itself, leaving logged frames plaintext in memory but not on the wire. - tlsPlan []*tls.Config - attempts map[string]int - errChan chan error -} - -func NewProxy() Proxy { - dialer := net.Dialer{} - return Proxy{ - dialContext: dialer.DialContext, - attempts: map[string]int{}, - errChan: make(chan error, 1), - } -} - -func SetupPGX(config *pgx.ConnConfig) { - tlsPlan := []*tls.Config{config.TLSConfig} - for _, fallback := range config.Fallbacks { - if fallback.Host != config.Host { - break - } - // ConnectByUrl drops plaintext fallbacks once the primary is TLS, and it - // runs after this, so apply the same rule here or --debug reinstates them. - if config.TLSConfig != nil && fallback.TLSConfig == nil { - continue - } - tlsPlan = append(tlsPlan, fallback.TLSConfig) - } - proxy := Proxy{ - dialContext: config.DialFunc, - tlsPlan: tlsPlan, - attempts: map[string]int{}, - errChan: make(chan error, 1), - } - config.DialFunc = proxy.DialFunc - // pgx must not negotiate TLS again over the in-memory pipe; clearing these - // *without* DialFunc's handshake is what downgraded sslmode=require (#5872). - config.TLSConfig = nil - // pgconn reads Fallbacks independently of TLSConfig, so a restored sslmode=allow - // would still retry TLS through this plaintext proxy. - for _, fallback := range config.Fallbacks { - fallback.TLSConfig = nil - } -} - -// nextTLSConfig returns the TLS config for this attempt against addr. pgconn dials -// each plan entry once per resolved address, in plan order, so a per-address -// counter replays the same TLS/plaintext sequence it would have used itself. -func (p *Proxy) nextTLSConfig(addr string) *tls.Config { - if p.attempts == nil { - p.attempts = map[string]int{} - } - if len(p.tlsPlan) == 0 { - return nil - } - i := p.attempts[addr] - p.attempts[addr]++ - // pgconn redials an addr for the target_session_attrs retry, so clamp rather - // than fall off the plan into plaintext. - if i >= len(p.tlsPlan) { - i = len(p.tlsPlan) - 1 - } - return p.tlsPlan[i] -} - -func (p *Proxy) DialFunc(ctx context.Context, network, addr string) (net.Conn, error) { - tlsConfig := p.nextTLSConfig(addr) - serverConn, err := p.dialContext(ctx, network, addr) - if err != nil { - return nil, err - } - if tlsConfig != nil { - if serverConn, err = startTLS(ctx, serverConn, tlsConfig); err != nil { - return nil, err - } - } - - const bufSize = 1024 * 1024 - ln := bufconn.Listen(bufSize) - go func() { - defer serverConn.Close() - clientConn, err := ln.Accept() - if err != nil { - // Unreachable code as bufconn never throws, but just in case - panic(err) - } - defer clientConn.Close() - - backend := NewBackend(clientConn) - frontend := NewFrontend(serverConn) - go backend.forward(frontend, p.errChan) - go frontend.forward(backend, p.errChan) - - for { - if err := <-p.errChan; err != nil && - // Since pgx closes connection first, every EOF is seen as unexpected - !errors.Is(err, io.ErrUnexpectedEOF) && - // Frontend might receive a reply after pgx closes the connection, in - // which case the backend will write to a closed pipe. So ignore. - !errors.Is(err, io.ErrClosedPipe) { - panic(err) - } - } - }() - - return ln.DialContext(ctx) -} - -// libpq's SSLRequest message code, PG_PROTOCOL(1234,5679). -const sslRequestCode = 80877103 - -// startTLS mirrors pgconn's own startTLS (pgconn@v1.14.3/pgconn.go:406-419), but -// handshakes eagerly so a refusal or bad certificate fails the dial rather than -// surfacing as a parse error inside the forwarding goroutines. -func startTLS(ctx context.Context, conn net.Conn, tlsConfig *tls.Config) (net.Conn, error) { - // pgconn only watches ctx once DialFunc returns, so bound the negotiation here - // or an endpoint that accepts TCP and never replies hangs the read forever. - if deadline, ok := ctx.Deadline(); ok { - if err := conn.SetDeadline(deadline); err != nil { - conn.Close() - return nil, err - } - defer func() { _ = conn.SetDeadline(time.Time{}) }() - } - if err := binary.Write(conn, binary.BigEndian, []int32{8, sslRequestCode}); err != nil { - conn.Close() - return nil, err - } - response := make([]byte, 1) - if _, err := io.ReadFull(conn, response); err != nil { - conn.Close() - return nil, err - } - if response[0] != 'S' { - conn.Close() - return nil, errors.New("server refused TLS connection") - } - tlsConn := tls.Client(conn, tlsConfig) - if err := tlsConn.HandshakeContext(ctx); err != nil { - conn.Close() - return nil, err - } - return tlsConn, nil -} - -type Backend struct { - *pgproto3.Backend - logger *log.Logger -} - -func NewBackend(clientConn net.Conn) Backend { - return Backend{ - pgproto3.NewBackend(pgproto3.NewChunkReader(clientConn), clientConn), - log.New(os.Stderr, "PG Recv: ", log.LstdFlags|log.Lmsgprefix), - } -} - -func (b *Backend) forward(frontend Frontend, errChan chan error) { - startupMessage, err := b.ReceiveStartupMessage() - if err != nil { - errChan <- err - return - } - - buf, err := json.Marshal(startupMessage) - if err != nil { - errChan <- err - return - } - frontend.logger.Println(string(buf)) - - if err = frontend.Send(startupMessage); err != nil { - errChan <- err - return - } - - for { - msg, err := b.Receive() - if err != nil { - errChan <- err - return - } - - buf, err := json.Marshal(msg) - if err != nil { - errChan <- err - return - } - frontend.logger.Println(string(buf)) - - if err = frontend.Send(msg); err != nil { - errChan <- err - return - } - } -} - -type Frontend struct { - *pgproto3.Frontend - logger *log.Logger -} - -func NewFrontend(serverConn net.Conn) Frontend { - return Frontend{ - pgproto3.NewFrontend(pgproto3.NewChunkReader(serverConn), serverConn), - log.New(os.Stderr, "PG Send: ", log.LstdFlags|log.Lmsgprefix), - } -} - -func (f *Frontend) forward(backend Backend, errChan chan error) { - for { - msg, err := f.Receive() - if err != nil { - errChan <- err - return - } - - buf, err := json.Marshal(msg) - if err != nil { - errChan <- err - return - } - backend.logger.Println(string(buf)) - - if _, ok := msg.(pgproto3.AuthenticationResponseMessage); ok { - // Set the authentication type so the next backend.Receive() will - // properly decode the appropriate 'p' message. - if err := backend.SetAuthType(f.GetAuthType()); err != nil { - errChan <- err - return - } - } - - if err := backend.Send(msg); err != nil { - errChan <- err - return - } - } -} diff --git a/apps/cli-go/internal/debug/postgres_test.go b/apps/cli-go/internal/debug/postgres_test.go deleted file mode 100644 index 87a29c3dbe..0000000000 --- a/apps/cli-go/internal/debug/postgres_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package debug - -import ( - "context" - "encoding/binary" - "io" - "net" - "testing" - - "github.com/jackc/pgx/v4" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/pkg/pgtest" -) - -func TestPostgresProxy(t *testing.T) { - const postgresUrl = "postgresql://postgres:password@127.0.0.1:5432/postgres" - - t.Run("forwards messages between frontend and backend", func(t *testing.T) { - // Parse connection url - config, err := pgx.ParseConfig(postgresUrl) - require.NoError(t, err) - // Setup postgres mock - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Intercept(config) - // Run test - SetupPGX(config) - ctx := context.Background() - proxy, err := pgx.ConnectConfig(ctx, config) - assert.NoError(t, err) - assert.NoError(t, proxy.Close(ctx)) - }) - - t.Run("negotiates TLS itself instead of downgrading", func(t *testing.T) { - config, err := pgx.ParseConfig(postgresUrl + "?sslmode=require") - require.NoError(t, err) - require.NotNil(t, config.TLSConfig) - // Run test - SetupPGX(config) - assert.Nil(t, config.TLSConfig) - for _, fallback := range config.Fallbacks { - assert.Nil(t, fallback.TLSConfig) - } - // A server refusing TLS must fail the dial, not continue in plaintext. - ln, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - defer ln.Close() - go func() { - conn, err := ln.Accept() - if err != nil { - return - } - defer conn.Close() - request := make([]byte, 8) - if _, err := io.ReadFull(conn, request); err != nil { - return - } - assert.Equal(t, uint32(sslRequestCode), binary.BigEndian.Uint32(request[4:])) - _, _ = conn.Write([]byte("N")) - }() - _, err = config.DialFunc(context.Background(), "tcp", ln.Addr().String()) - assert.ErrorContains(t, err, "server refused TLS connection") - }) -} diff --git a/apps/cli-go/internal/functions/download/download.go b/apps/cli-go/internal/functions/download/download.go deleted file mode 100644 index 0f497ee699..0000000000 --- a/apps/cli-go/internal/functions/download/download.go +++ /dev/null @@ -1,650 +0,0 @@ -package download - -import ( - "bufio" - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "mime" - "mime/multipart" - "net/http" - "net/textproto" - "net/url" - "os" - "os/exec" - "path" - "path/filepath" - "strings" - - "github.com/andybalholm/brotli" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/network" - "github.com/docker/go-units" - "github.com/go-errors/errors" - "github.com/google/uuid" - "github.com/spf13/afero" - "github.com/spf13/viper" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/internal/utils/flags" - "github.com/supabase/cli/pkg/api" -) - -var ( - legacyEntrypointPath = "file:///src/index.ts" - legacyImportMapPath = "file:///src/import_map.json" -) - -// ErrUnsafeDownloadPath is returned when a downloaded Function file's path, -// as reported by the server via the Supabase-Path header or the -// Content-Disposition filename, would resolve outside utils.FunctionsDir -// once joined and cleaned, or would require following an existing symlink -// to get there. The server response is not trusted input, so any path that -// looks like it's trying to escape the functions directory is rejected -// rather than sanitized. -var ErrUnsafeDownloadPath = errors.New("invalid path in server response") - -func RunLegacy(ctx context.Context, slug string, projectRef string, fsys afero.Fs) error { - // 1. Sanity checks. - { - if err := utils.ValidateFunctionSlug(slug); err != nil { - return err - } - } - if err := utils.InstallOrUpgradeDeno(ctx, fsys); err != nil { - return err - } - - scriptDir, err := utils.CopyDenoScripts(ctx, fsys) - if err != nil { - return err - } - - // 2. Download Function. - if err := downloadFunction(ctx, projectRef, slug, scriptDir.ExtractPath); err != nil { - return err - } - - fmt.Fprintf(os.Stderr, "Downloaded Function %s from project %s.\n", utils.Aqua(slug), utils.Aqua(projectRef)) - return nil -} - -func getFunctionMetadata(ctx context.Context, projectRef, slug string) (*api.FunctionSlugResponseOutput, error) { - resp, err := utils.GetSupabase().V1GetAFunctionWithResponse(ctx, projectRef, slug) - if err != nil { - return nil, errors.Errorf("failed to get function metadata: %w", err) - } - - switch resp.StatusCode() { - case http.StatusNotFound: - return nil, errors.Errorf("Function %s does not exist on the Supabase project.", utils.Aqua(slug)) - case http.StatusOK: - break - default: - return nil, errors.Errorf("Failed to download Function %s on the Supabase project: %s", utils.Aqua(slug), string(resp.Body)) - } - - if resp.JSON200.EntrypointPath == nil { - resp.JSON200.EntrypointPath = &legacyEntrypointPath - } - if resp.JSON200.ImportMapPath == nil { - resp.JSON200.ImportMapPath = &legacyImportMapPath - } - return resp.JSON200, nil -} - -func downloadFunction(ctx context.Context, projectRef, slug, extractScriptPath string) error { - fmt.Fprintln(os.Stderr, "Downloading function:", utils.Bold(slug)) - denoPath, err := utils.GetDenoPath() - if err != nil { - return err - } - - meta, err := getFunctionMetadata(ctx, projectRef, slug) - if err != nil { - return err - } - - resp, err := utils.GetSupabase().V1GetAFunctionBodyWithResponse(ctx, projectRef, slug) - if err != nil { - return errors.Errorf("failed to get function body: %w", err) - } - if resp.StatusCode() != http.StatusOK { - return errors.New("Unexpected error downloading Function: " + string(resp.Body)) - } - - resBuf := bytes.NewReader(resp.Body) - funcDir := filepath.Join(utils.FunctionsDir, slug) - args := []string{"run", "-A", extractScriptPath, funcDir, *meta.EntrypointPath, utils.FunctionsDir} - cmd := exec.CommandContext(ctx, denoPath, args...) - var errBuf bytes.Buffer - cmd.Stdin = resBuf - cmd.Stdout = os.Stdout - cmd.Stderr = &errBuf - if err := cmd.Run(); err != nil { - return errors.Errorf("Error downloading function: %w\n%v", err, errBuf.String()) - } - return nil -} - -func Run(ctx context.Context, slug, projectRef string, useLegacyBundle, useDocker bool, fsys afero.Fs) error { - // Sanity check - if err := flags.LoadConfig(fsys); err != nil { - return err - } - - // Defaults to server-side unbundling with multipart/form-data - downloader := downloadWithServerSideUnbundle - if useLegacyBundle { - downloader = RunLegacy - } else if useDocker { - if utils.IsDockerRunning(ctx) { - // Download eszip file for client-side unbundling with edge-runtime - downloader = downloadWithDockerUnbundle - } else { - fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "Docker is not running") - } - } - - if len(slug) > 0 { - return downloader(ctx, slug, projectRef, fsys) - } - return downloadAll(ctx, projectRef, fsys, downloader) -} - -func downloadAll(ctx context.Context, projectRef string, fsys afero.Fs, downloader func(context.Context, string, string, afero.Fs) error) error { - resp, err := utils.GetSupabase().V1ListAllFunctionsWithResponse(ctx, projectRef) - if err != nil { - return errors.Errorf("failed to list functions: %w", err) - } - if resp.JSON200 == nil { - return errors.Errorf("unexpected list functions status %d: %s", resp.StatusCode(), string(resp.Body)) - } - - functions := *resp.JSON200 - if len(functions) == 0 { - fmt.Fprintln(os.Stderr, "No functions found in project ", utils.Aqua(projectRef)) - return nil - } - - fmt.Fprintf(os.Stderr, "Found %d function(s) to download\n", len(functions)) - for _, f := range functions { - // f.Slug comes straight from the Management API response, which - // this threat model treats as untrusted: a malicious or corrupted - // response (or a MITM) could return a slug containing ".." or "/" - // segments. Every downloader below joins this value into a - // filesystem path (utils.TempDir for downloadOne, utils.FunctionsDir - // for downloadWithServerSideUnbundle) before any validation of its - // own, so it must be rejected here -- the single point where it - // enters this dispatch logic -- rather than relying on each - // downstream path-construction site to defend itself. - if err := utils.ValidateFunctionSlug(f.Slug); err != nil { - utils.CmdSuggestion = fmt.Sprintf( - "The Supabase API returned an unexpected function slug (%s). Retry the command, and if this keeps happening, verify your network connection is not being intercepted before contacting Supabase support.", - utils.Aqua(f.Slug), - ) - return errors.Errorf("failed to download function %s: %w", f.Slug, err) - } - if err := downloader(ctx, f.Slug, projectRef, fsys); err != nil { - return err - } - } - - fmt.Fprintln(os.Stderr, "Successfully downloaded all functions from project", utils.Aqua(projectRef)) - return nil -} - -func downloadWithDockerUnbundle(ctx context.Context, slug string, projectRef string, fsys afero.Fs) error { - eszipPath, err := downloadOne(ctx, slug, projectRef, fsys) - if err != nil { - return err - } - if !viper.GetBool("DEBUG") { - defer func() { - if err := fsys.Remove(eszipPath); err != nil { - fmt.Fprintln(os.Stderr, err) - } - }() - } - // Extract eszip to functions directory - err = extractOne(ctx, slug, eszipPath) - if err != nil { - utils.CmdSuggestion += suggestLegacyBundle(slug) - } - return err -} - -func downloadOne(ctx context.Context, slug, projectRef string, fsys afero.Fs) (string, error) { - fmt.Fprintln(os.Stderr, "Downloading function:", utils.Bold(slug)) - resp, err := utils.GetSupabase().V1GetAFunctionBody(ctx, projectRef, slug) - if err != nil { - return "", errors.Errorf("failed to get function body: %w", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", errors.Errorf("Error status %d: unexpected error downloading Function", resp.StatusCode) - } - return "", errors.Errorf("Error status %d: %s", resp.StatusCode, string(body)) - } - r := io.Reader(resp.Body) - if strings.EqualFold(resp.Header.Get("Content-Encoding"), "br") { - r = brotli.NewReader(resp.Body) - } - // Create temp file to store downloaded eszip - eszipPath := filepath.Join(utils.TempDir, fmt.Sprintf("output_%s.eszip", slug)) - if err := utils.MkdirIfNotExistFS(fsys, utils.TempDir); err != nil { - return "", err - } - if err := afero.WriteReader(fsys, eszipPath, r); err != nil { - return "", errors.Errorf("failed to download file: %w", err) - } - return eszipPath, nil -} - -func extractOne(ctx context.Context, slug, eszipPath string) error { - hostFuncDirPath, err := filepath.Abs(utils.FunctionsDir) - if err != nil { - return errors.Errorf("failed to resolve functions path: %w", err) - } - - hostEszipPath, err := filepath.Abs(eszipPath) - if err != nil { - return errors.Errorf("failed to resolve eszip path: %w", err) - } - dockerEszipPath := path.Join(utils.DockerEszipDir, filepath.Base(hostEszipPath)) - dockerOutputPath := path.Join(utils.DockerDenoDir, slug) - - binds := []string{ - // Reuse deno cache directory, ie. DENO_DIR, between container restarts - // https://denolib.gitbook.io/guide/advanced/deno_dir-code-fetch-and-cache - utils.EdgeRuntimeId + ":/root/.cache/deno:rw", - hostEszipPath + ":" + dockerEszipPath + ":ro", - hostFuncDirPath + ":" + utils.DockerDenoDir + ":rw", - } - - return utils.DockerRunOnceWithConfig( - ctx, - container.Config{ - Image: utils.Config.EdgeRuntime.Image, - Cmd: []string{"unbundle", "--eszip", dockerEszipPath, "--output", dockerOutputPath}, - }, - container.HostConfig{ - Binds: binds, - }, - network.NetworkingConfig{}, - "", - os.Stdout, - getErrorLogger(), - ) -} - -func getErrorLogger() io.Writer { - if utils.Config.EdgeRuntime.DenoVersion > 1 { - return os.Stderr - } - // Additional error handling for deno v1 - r, w := io.Pipe() - go func() { - logs := bufio.NewScanner(r) - for logs.Scan() { - line := logs.Text() - fmt.Fprintln(os.Stderr, line) - if strings.EqualFold(line, "invalid eszip v2") { - utils.CmdSuggestion = suggestDenoV2() - } - } - if err := logs.Err(); err != nil { - fmt.Fprintln(os.Stderr, err) - } - }() - return w -} - -func suggestDenoV2() string { - return fmt.Sprintf(`Please use deno v2 in %s to download this Function: - -[edge_runtime] -deno_version = 2 -`, utils.Bold(utils.ConfigPath)) -} - -func suggestLegacyBundle(slug string) string { - return fmt.Sprintf("\nIf your function is deployed using CLI < 1.120.0, trying running %s instead.", utils.Aqua("supabase functions download --legacy-bundle "+slug)) -} - -func suggestUnsafeDownloadPath() string { - return "This usually indicates a malformed or unexpected API response. If you're using a self-hosted instance, verify your API URL is correct." -} - -type bundleMetadata struct { - EntrypointPath string `json:"deno2_entrypoint_path,omitempty"` -} - -// New server-side unbundle implementation that mirrors Studio's entrypoint-based -// base-dir + relative path behaviour. -func downloadWithServerSideUnbundle(ctx context.Context, slug, projectRef string, fsys afero.Fs) error { - fmt.Fprintln(os.Stderr, "Downloading Function:", utils.Bold(slug)) - - form, err := readForm(ctx, projectRef, slug) - if err != nil { - return err - } - defer func() { - if err := form.RemoveAll(); err != nil { - fmt.Fprintln(os.Stderr, err) - } - }() - - // Read entrypoint path from deno2 bundles - metadata := bundleMetadata{} - if data, ok := form.Value["metadata"]; ok { - for _, part := range data { - if err := json.Unmarshal([]byte(part), &metadata); err != nil { - return errors.Errorf("failed to unmarshal metadata: %w", err) - } - } - } - - // Fallback to function metadata from upstash - if len(metadata.EntrypointPath) == 0 { - upstash, err := getFunctionMetadata(ctx, projectRef, slug) - if err != nil { - return errors.Errorf("failed to get function metadata: %w", err) - } - entrypointUrl, err := url.Parse(*upstash.EntrypointPath) - if err != nil { - return errors.Errorf("failed to parse entrypoint URL: %w", err) - } - metadata.EntrypointPath = entrypointUrl.Path - } - fmt.Fprintln(utils.GetDebugLogger(), "Using entrypoint path:", metadata.EntrypointPath) - - // Root directory on disk: supabase/functions/ - funcDir := filepath.Join(utils.FunctionsDir, slug) - for _, data := range form.File { - for _, file := range data { - if err := saveFile(file, metadata.EntrypointPath, funcDir, fsys); err != nil { - return err - } - } - } - - fmt.Fprintf(os.Stderr, "Downloaded Function %s from project %s.\n", utils.Aqua(slug), utils.Aqua(projectRef)) - return nil -} - -func readForm(ctx context.Context, projectRef, slug string) (*multipart.Form, error) { - // Request multipart/form-data response using RequestEditorFn - resp, err := utils.GetSupabase().V1GetAFunctionBody(ctx, projectRef, slug, func(ctx context.Context, req *http.Request) error { - req.Header.Set("Accept", "multipart/form-data") - return nil - }) - if err != nil { - return nil, errors.Errorf("failed to download function: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, errors.Errorf("Error status %d: %w", resp.StatusCode, err) - } - return nil, errors.Errorf("Error status %d: %s", resp.StatusCode, string(body)) - } - - // Parse the multipart response - mediaType, params, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) - if err != nil { - return nil, errors.Errorf("failed to parse content type: %w", err) - } - if !strings.HasPrefix(mediaType, "multipart/") { - return nil, errors.Errorf("expected multipart response, got %s", mediaType) - } - - // Read entire response with caching to disk - mr := multipart.NewReader(resp.Body, params["boundary"]) - form, err := mr.ReadForm(units.MiB) - if err != nil { - return nil, errors.Errorf("failed to read form: %w", err) - } - - return form, nil -} - -func saveFile(file *multipart.FileHeader, entrypointPath, funcDir string, fsys afero.Fs) error { - part, err := file.Open() - if err != nil { - return errors.Errorf("failed to open file: %w", err) - } - defer part.Close() - - logger := utils.GetDebugLogger() - partPath, err := getPartPath(file.Header) - if len(partPath) == 0 { - fmt.Fprintln(logger, "Skipping file with empty path:", file.Filename) - return err - } - fmt.Fprintln(logger, "Resolving file path:", partPath) - - relPath, err := filepath.Rel(filepath.FromSlash(entrypointPath), filepath.FromSlash(partPath)) - if err != nil { - // Continue extracting without entrypoint. Go's Rel error embeds - // both paths verbatim ("Rel: can't make relative to - // "), and partPath is server-controlled, so this is gated - // behind --debug like the "Resolving file path" log above rather - // than printed unconditionally to stderr. - fmt.Fprintln(logger, "WARNING:", err) - relPath = filepath.FromSlash(path.Join("..", partPath)) - } - - // partPath (and therefore relPath and dstPath) is derived from - // server-controlled multipart metadata (Supabase-Path header or - // Content-Disposition filename), so it must be validated before it - // touches the filesystem below. - dstPath := filepath.Join(funcDir, path.Base(entrypointPath), relPath) - - // Containment is enforced against the shared functions directory - // rather than funcDir: the entrypoint-mismatch fallback above can - // legitimately resolve one level above funcDir (into a sibling - // function's directory), but must never resolve outside - // utils.FunctionsDir entirely, e.g. via a "../../../etc/passwd" style - // payload. - root := utils.FunctionsDir - if err := validateDownloadPath(root, dstPath); err != nil { - utils.CmdSuggestion = suggestUnsafeDownloadPath() - return err - } - - // A previous part could have planted a symlink at an intermediate - // directory component, which would otherwise let MkdirAll/OpenFile - // silently follow it out of root even though dstPath itself looks - // clean. Check before creating the directory, and again after the - // write lands in case a symlink was swapped in between the two checks. - dstDir := filepath.Dir(dstPath) - if err := ensureNoSymlinkInPath(fsys, root, dstDir); err != nil { - utils.CmdSuggestion = suggestUnsafeDownloadPath() - return err - } - if err := utils.MkdirIfNotExistFS(fsys, dstDir); err != nil { - return err - } - - fmt.Fprintln(os.Stderr, "Extracting file:", dstPath) - if err := writeFileNoFollowSymlink(fsys, dstPath, part); err != nil { - return err - } - - if err := ensureNoSymlinkInPath(fsys, root, dstPath); err != nil { - utils.CmdSuggestion = suggestUnsafeDownloadPath() - return err - } - return nil -} - -// validateDownloadPath rejects dstPath if, once lexically cleaned, it does -// not resolve inside root. This is the primary defense against a hostile -// "../../etc/passwd" style Supabase-Path/filename escaping the functions -// directory: filepath.Join already cleans ".." segments away, so this -// check must run against the cleaned result rather than by scanning the -// raw, attacker-controlled path for "..". -func validateDownloadPath(root, dstPath string) error { - rel, err := filepath.Rel(root, filepath.Clean(dstPath)) - if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) || filepath.IsAbs(rel) { - return errors.Errorf("failed to save file: %w", ErrUnsafeDownloadPath) - } - return nil -} - -// ensureNoSymlinkInPath rejects dir if resolving the symlinks along it would -// land outside root. -// -// Earlier versions of this check rejected the mere presence of a symlink -// anywhere under root, which also broke legitimate setups such as a -// monorepo symlinking a shared directory into place inside the functions -// tree. Instead, this resolves both root and dir the same way -- walking up -// to the deepest ancestor that already exists on disk, following any chain -// of symlinks there via filepath.EvalSymlinks (this also naturally covers -// root itself being a symlink, since dir's walk passes through root), and -// re-joining the still-nonexistent remainder back on -- and re-validates -// the two resolved paths against each other with validateDownloadPath. -// Resolving root the same way dir is resolved, rather than comparing a -// resolved dir against a literal root, matters in practice: it is what -// keeps an OS-level symlink that sits above both of them (e.g. macOS's -// /var -> /private/var, which every path under a t.TempDir() passes -// through) from producing a spurious mismatch. Only a resolved dir that -// escapes the resolved root is rejected; a symlink whose target still -// lands inside root, however deeply nested, is now allowed. -// -// This only has an effect on filesystems that expose real symlink -// semantics through afero.LinkReader, i.e. afero.NewOsFs in production -// (afero.Lstater is not a reliable enough signal on its own: afero.MemMapFs -// also implements it, just by delegating straight to Stat). The in-memory -// filesystem used in tests implements neither, so this check is -// effectively a no-op there -- there is nothing to protect against on a -// filesystem that cannot contain symlinks in the first place. That same -// guard is also why it is safe for filepath.EvalSymlinks below to hit the -// real OS filesystem directly instead of going through fsys: the only -// afero.Fs implementation used in production, afero.NewOsFs, delegates to -// the real OS filesystem for every operation, so the two agree. -func ensureNoSymlinkInPath(fsys afero.Fs, root, dir string) error { - if _, ok := fsys.(afero.LinkReader); !ok { - return nil - } - - // filepath.EvalSymlinks returns an absolute result as soon as it - // crosses one absolute symlink, but stays relative otherwise (per its - // doc comment), so root and dir must both start out absolute here -- - // otherwise root (no symlink crossed) and dir (crossing one) could - // resolve to a relative and an absolute path respectively, which - // filepath.Rel below cannot meaningfully compare. - absRoot, err := filepath.Abs(root) - if err != nil { - return errors.Errorf("failed to inspect extraction path: %w", err) - } - absDir, err := filepath.Abs(dir) - if err != nil { - return errors.Errorf("failed to inspect extraction path: %w", err) - } - - resolvedRoot, err := resolveExistingPath(fsys, absRoot) - if err != nil { - return errors.Errorf("failed to inspect extraction path: %w", err) - } - resolvedDir, err := resolveExistingPath(fsys, absDir) - if err != nil { - return errors.Errorf("failed to inspect extraction path: %w", err) - } - - return validateDownloadPath(resolvedRoot, resolvedDir) -} - -// resolveExistingPath resolves p by walking up to the deepest ancestor of it -// that already exists -- a path component that does not exist yet cannot -// itself be a symlink, so there is nothing there for EvalSymlinks to -// resolve -- following any chain of symlinks in that ancestor to its real -// target, then re-joining the still-nonexistent remainder of p back onto -// the result. -func resolveExistingPath(fsys afero.Fs, p string) (string, error) { - existing := p - var suffix []string - for { - if _, err := fsys.Stat(existing); err == nil { - break - } else if !os.IsNotExist(err) { - return "", err - } - parent := filepath.Dir(existing) - if parent == existing { - // Reached the filesystem root without finding anything that - // exists yet, so there is nothing left to resolve. - return filepath.Join(append([]string{existing}, suffix...)...), nil - } - suffix = append([]string{filepath.Base(existing)}, suffix...) - existing = parent - } - - resolved, err := filepath.EvalSymlinks(existing) - if err != nil { - return "", err - } - return filepath.Join(append([]string{resolved}, suffix...)...), nil -} - -// writeFileNoFollowSymlink writes r to dstPath without following a symlink -// that might already occupy that path. It creates a randomly named -// temporary file next to dstPath with O_EXCL, refusing to write through -// anything already there, then atomically renames it onto dstPath. -// Rename replaces whatever directory entry currently exists at dstPath -- -// including a symlink -- rather than dereferencing it, which is exactly -// what a plain fs.Create/afero.WriteReader would otherwise do. -func writeFileNoFollowSymlink(fsys afero.Fs, dstPath string, r io.Reader) error { - tmpPath := filepath.Join(filepath.Dir(dstPath), fmt.Sprintf(".supabase-download-%s.tmp", uuid.NewString())) - tmp, err := fsys.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) - if err != nil { - return errors.Errorf("failed to save file: %w", err) - } - - if _, err := io.Copy(tmp, r); err != nil { - _ = tmp.Close() - _ = fsys.Remove(tmpPath) - return errors.Errorf("failed to save file: %w", err) - } - if err := tmp.Close(); err != nil { - _ = fsys.Remove(tmpPath) - return errors.Errorf("failed to save file: %w", err) - } - - if err := fsys.Rename(tmpPath, dstPath); err != nil { - _ = fsys.Remove(tmpPath) - return errors.Errorf("failed to save file: %w", err) - } - return nil -} - -// getPartPath extracts the filename for a multipart part, allowing for -// relative paths via the custom Supabase-Path header. -func getPartPath(header textproto.MIMEHeader) (string, error) { - // dedicated header to specify relative path, not expected to be used - if relPath := header.Get("Supabase-Path"); relPath != "" { - return relPath, nil - } - - // part.FileName() does not allow us to handle relative paths, so we parse Content-Disposition manually - cd := header.Get("Content-Disposition") - if cd == "" { - return "", nil - } - - _, params, err := mime.ParseMediaType(cd) - if err != nil { - return "", errors.Errorf("failed to parse content disposition: %w", err) - } - - if filename := params["filename"]; filename != "" { - return filename, nil - } - return "", nil -} diff --git a/apps/cli-go/internal/functions/download/download_test.go b/apps/cli-go/internal/functions/download/download_test.go deleted file mode 100644 index 8267dc7eeb..0000000000 --- a/apps/cli-go/internal/functions/download/download_test.go +++ /dev/null @@ -1,914 +0,0 @@ -package download - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "log" - "mime/multipart" - "net/http" - "net/textproto" - "os" - "path" - "path/filepath" - "strings" - "testing" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/volume" - "github.com/h2non/gock" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/testing/apitest" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/internal/utils/flags" - "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/cast" -) - -func TestMain(m *testing.M) { - // Setup fake deno binary - if len(os.Args) > 1 && (os.Args[1] == "bundle" || os.Args[1] == "upgrade" || os.Args[1] == "run") { - msg := os.Getenv("TEST_DENO_ERROR") - if msg != "" { - fmt.Fprintln(os.Stderr, msg) - os.Exit(1) - } - os.Exit(0) - } - denoPath, err := os.Executable() - if err != nil { - log.Fatalln(err) - } - utils.DenoPathOverride = denoPath - // Run test suite - os.Exit(m.Run()) -} - -type multipartPart struct { - filename string - supabasePath string - contents string -} - -func mockMultipartBody(t *testing.T, projectRef, slug string, metadata bundleMetadata, parts []multipartPart) { - t.Helper() - var buf bytes.Buffer - writer := multipart.NewWriter(&buf) - // Write metadata - headers := textproto.MIMEHeader{} - headers.Set("Content-Disposition", `form-data; name="metadata"`) - headers.Set("Content-Type", "application/json") - pw, err := writer.CreatePart(headers) - require.NoError(t, err) - enc := json.NewEncoder(pw) - require.NoError(t, enc.Encode(metadata)) - // Write files - for _, part := range parts { - headers := textproto.MIMEHeader{} - headers.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, part.filename)) - if part.supabasePath != "" { - headers.Set("Supabase-Path", part.supabasePath) - } - pw, err := writer.CreatePart(headers) - require.NoError(t, err) - _, err = pw.Write([]byte(part.contents)) - require.NoError(t, err) - } - require.NoError(t, writer.Close()) - - gock.New(utils.DefaultApiHost). - Get(fmt.Sprintf("/v1/projects/%s/functions/%s/body", projectRef, slug)). - Reply(http.StatusOK). - SetHeader("Content-Type", writer.FormDataContentType()). - Body(&buf) -} - -func TestRunLegacyUnbundle(t *testing.T) { - const slug = "test-func" - - t.Run("downloads eszip bundle", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup valid project ref - project := apitest.RandomProjectRef() - // Setup valid access token - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) - // Setup valid deno path - _, err := fsys.Create(utils.DenoPathOverride) - require.NoError(t, err) - // Setup mock api - defer gock.OffAll() - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + project + "/functions/" + slug). - Reply(http.StatusOK). - JSON(api.FunctionResponseOutput{Id: "1"}) - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + project + "/functions/" + slug + "/body"). - Reply(http.StatusOK) - // Run test - err = Run(context.Background(), slug, project, true, false, fsys) - // Check error - assert.NoError(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on malformed slug", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup valid project ref - project := apitest.RandomProjectRef() - // Run test - err := Run(context.Background(), "@", project, true, false, fsys) - // Check error - assert.ErrorContains(t, err, "Invalid Function name.") - }) - - t.Run("throws error on failure to install deno", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewReadOnlyFs(afero.NewMemMapFs()) - // Setup valid project ref - project := apitest.RandomProjectRef() - // Run test - err := Run(context.Background(), slug, project, true, false, fsys) - // Check error - assert.ErrorContains(t, err, "operation not permitted") - }) - - t.Run("throws error on copy failure", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup valid project ref - project := apitest.RandomProjectRef() - // Setup valid deno path - _, err := fsys.Create(utils.DenoPathOverride) - require.NoError(t, err) - // Run test - err = Run(context.Background(), slug, project, true, false, afero.NewReadOnlyFs(fsys)) - // Check error - assert.ErrorContains(t, err, "operation not permitted") - }) - - t.Run("throws error on missing function", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup valid project ref - project := apitest.RandomProjectRef() - // Setup valid access token - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) - // Setup valid deno path - _, err := fsys.Create(utils.DenoPathOverride) - require.NoError(t, err) - // Setup mock api - defer gock.OffAll() - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + project + "/functions/" + slug). - Reply(http.StatusNotFound). - JSON(map[string]string{"message": "Function not found"}) - // Run test - err = Run(context.Background(), slug, project, true, false, fsys) - // Check error - assert.ErrorContains(t, err, "Function test-func does not exist on the Supabase project.") - }) -} - -func TestRunDockerUnbundle(t *testing.T) { - t.Run("downloads bundle with docker when available", func(t *testing.T) { - const slugDocker = "demo" - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - project := apitest.RandomProjectRef() - require.NoError(t, flags.LoadConfig(fsys)) - - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) - - require.NoError(t, apitest.MockDocker(utils.Docker)) - dockerHost := utils.Docker.DaemonHost() - - // Setup mock api - defer gock.OffAll() - - gock.New(dockerHost). - Head("/_ping"). - Reply(http.StatusOK) - - imageURL := utils.GetRegistryImageUrl(utils.Config.EdgeRuntime.Image) - containerID := "docker-unbundle-test" - var createRequest struct { - Cmd []string `json:"Cmd"` - HostConfig struct { - Binds []string `json:"Binds"` - } `json:"HostConfig"` - } - gock.New(dockerHost). - Get("/v" + utils.Docker.ClientVersion() + "/images/" + imageURL + "/json"). - Reply(http.StatusOK). - JSON(image.InspectResponse{}) - gock.New(dockerHost). - Post("/v" + utils.Docker.ClientVersion() + "/networks/create"). - Reply(http.StatusCreated). - JSON(network.CreateResponse{}) - gock.New(dockerHost). - Post("/v" + utils.Docker.ClientVersion() + "/volumes/create"). - Persist(). - Reply(http.StatusCreated). - JSON(volume.Volume{}) - gock.New(dockerHost). - Post("/v" + utils.Docker.ClientVersion() + "/containers/create"). - AddMatcher(func(req *http.Request, ereq *gock.Request) (bool, error) { - body, err := io.ReadAll(req.Body) - if err != nil { - return false, err - } - return true, json.Unmarshal(body, &createRequest) - }). - Reply(http.StatusOK). - JSON(container.CreateResponse{ID: containerID}) - gock.New(dockerHost). - Post("/v" + utils.Docker.ClientVersion() + "/containers/" + containerID + "/start"). - Reply(http.StatusAccepted) - require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerID, "unbundle ok")) - - gock.New(utils.DefaultApiHost). - Get(fmt.Sprintf("/v1/projects/%s/functions/%s/body", project, slugDocker)). - Reply(http.StatusOK). - BodyString("fake eszip payload") - - err := Run(context.Background(), slugDocker, project, false, true, fsys) - require.NoError(t, err) - - eszipPath := filepath.Join(utils.TempDir, fmt.Sprintf("output_%s.eszip", slugDocker)) - exists, err := afero.Exists(fsys, eszipPath) - require.NoError(t, err) - assert.False(t, exists, "temporary eszip file should be removed after extraction") - - hostFunctionsDirPath, err := filepath.Abs(utils.FunctionsDir) - require.NoError(t, err) - hostEszipPath, err := filepath.Abs(eszipPath) - require.NoError(t, err) - assert.EqualValues(t, []string{ - "unbundle", - "--eszip", - path.Join(utils.DockerEszipDir, filepath.Base(hostEszipPath)), - "--output", - path.Join(utils.DockerDenoDir, slugDocker), - }, createRequest.Cmd) - assert.Contains(t, createRequest.HostConfig.Binds, utils.EdgeRuntimeId+":/root/.cache/deno:rw") - assert.Contains(t, createRequest.HostConfig.Binds, hostEszipPath+":"+path.Join(utils.DockerEszipDir, filepath.Base(hostEszipPath))+":ro") - assert.Contains(t, createRequest.HostConfig.Binds, hostFunctionsDirPath+":"+utils.DockerDenoDir+":rw") - for _, bind := range createRequest.HostConfig.Binds { - assert.False(t, strings.Contains(bind, filepath.Join(hostFunctionsDirPath, slugDocker)+":"+utils.DockerDenoDir), - "docker output should mount supabase/functions, not the slug directory") - } - - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("falls back to server-side unbundle when docker unavailable", func(t *testing.T) { - const slugDocker = "demo-fallback" - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - project := apitest.RandomProjectRef() - require.NoError(t, flags.LoadConfig(fsys)) - - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) - - require.NoError(t, apitest.MockDocker(utils.Docker)) - dockerHost := utils.Docker.DaemonHost() - - // Setup mock api - defer gock.OffAll() - - gock.New(dockerHost). - Head("/_ping"). - ReplyError(errors.New("docker unavailable")) - - mockMultipartBody(t, project, slugDocker, bundleMetadata{"/source/index.ts"}, []multipartPart{ - {filename: "/source/index.ts", contents: "console.log('hello')"}, - }) - - err := Run(context.Background(), slugDocker, project, false, true, fsys) - require.NoError(t, err) - - data, err := afero.ReadFile(fsys, filepath.Join(utils.FunctionsDir, slugDocker, "index.ts")) - require.NoError(t, err) - assert.Equal(t, "console.log('hello')", string(data)) - - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} - -// TestDownloadAllRejectsMaliciousSlug is a regression test for CLI-1891: the -// per-function loop in downloadAll must reject a path-traversal payload in -// a function's Slug -- as returned by V1ListAllFunctionsWithResponse, which -// this threat model treats as untrusted (a malicious/compromised Management -// API response, or a MITM) -- before handing it to any downloader that -// joins it into a filesystem path. -// -// This mirrors an exploit independently confirmed against downloadOne: with -// slug = "../../../../../poc-escaped-outside-project", downloadOne's -// eszipPath := filepath.Join(utils.TempDir, fmt.Sprintf("output_%s.eszip", -// slug)) resolves (after filepath.Clean) to "../poc-escaped-outside-project.eszip", -// i.e. one directory level above the project root, and -// afero.WriteReader happily MkdirAll's its way there and writes the -// server-controlled response body outside the sandbox. -func TestDownloadAllRejectsMaliciousSlug(t *testing.T) { - const maliciousSlug = "../../../../../poc-escaped-outside-project" - - // Use a real OS filesystem rooted at an isolated temp directory so an - // escape can actually be observed landing outside the project root, - // exactly as in the reviewer's PoC. - tmpDir := t.TempDir() - t.Chdir(tmpDir) - fsys := afero.NewOsFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - - project := apitest.RandomProjectRef() - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) - - utils.CmdSuggestion = "" - t.Cleanup(func() { utils.CmdSuggestion = "" }) - - defer gock.OffAll() - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + project + "/functions"). - Reply(http.StatusOK). - JSON([]api.FunctionResponseOutput{{ - Id: "poc-id", - Name: "poc", - Slug: maliciousSlug, - }}) - // Mocked so that, if the fix is removed, the malicious slug's request - // still succeeds and downloadOne proceeds all the way to writing the - // escaped file -- proving the escape, rather than masking it behind an - // unrelated network error. With the fix in place this mock is never hit. - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + project + "/functions/.*/body"). - Reply(http.StatusOK). - BodyString("fake eszip payload") - - // downloadOne is the exact sink the reviewer's PoC targeted directly; - // wrap it as a downloader so downloadAll's dispatch is exercised against - // the real vulnerable code, without also pulling in downloadWithDockerUnbundle's - // unrelated Docker extraction step (and its defer-cleanup of the eszip - // file, which would otherwise remove the escaped file before this test - // can observe it). - downloaderCalled := false - downloader := func(ctx context.Context, slug, projectRef string, fsys afero.Fs) error { - downloaderCalled = true - _, err := downloadOne(ctx, slug, projectRef, fsys) - return err - } - - err := downloadAll(context.Background(), project, fsys, downloader) - - // Check for the escape before any assertion below that could halt the - // test on failure (e.g. require.Error), so this is verified regardless - // of whether downloadAll happened to return an error for some other - // reason. t.TempDir()'s own cleanup RemoveAll's tmpDir's parent, which - // would otherwise sweep away the evidence once the test returns. - // - // The exploit resolves to "../poc-escaped-outside-project.eszip" - // relative to utils.TempDir, i.e. one level above the project root. - escapedPath := filepath.Join(tmpDir, "..", "poc-escaped-outside-project.eszip") - t.Cleanup(func() { _ = os.Remove(escapedPath) }) - exists, existsErr := afero.Exists(fsys, escapedPath) - require.NoError(t, existsErr) - assert.False(t, exists, "malicious slug must not be able to write outside the project directory") - - require.Error(t, err) - assert.ErrorIs(t, err, utils.ErrInvalidSlug) - assert.Contains(t, utils.CmdSuggestion, "unexpected function slug") - assert.False(t, downloaderCalled, "downloader must not be invoked with an unvalidated slug") - - assert.Empty(t, apitest.ListUnmatchedRequests()) -} - -func TestRunServerSideUnbundle(t *testing.T) { - const slug = "test-func" - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) - project := apitest.RandomProjectRef() - - t.Run("writes files using inferred base directory", func(t *testing.T) { - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - - defer gock.OffAll() - mockMultipartBody(t, project, slug, bundleMetadata{EntrypointPath: "source/index.ts"}, []multipartPart{ - {filename: "source/index.ts", contents: "console.log('hello')"}, - {filename: "source/utils.ts", contents: "export const value = 1;"}, - }) - - err := Run(context.Background(), slug, project, false, false, fsys) - require.NoError(t, err) - - data, err := afero.ReadFile(fsys, filepath.Join(utils.FunctionsDir, slug, "index.ts")) - require.NoError(t, err) - assert.Equal(t, "console.log('hello')", string(data)) - - data, err = afero.ReadFile(fsys, filepath.Join(utils.FunctionsDir, slug, "utils.ts")) - require.NoError(t, err) - assert.Equal(t, "export const value = 1;", string(data)) - - entries, err := afero.ReadDir(fsys, utils.TempDir) - if err == nil { - assert.Len(t, entries, 0, "expected temporary directory to be cleaned up") - } else { - assert.ErrorIs(t, err, os.ErrNotExist) - } - - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("derives base directory from absolute filenames", func(t *testing.T) { - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - - defer gock.OffAll() - indexPath := "/tmp/functions-download-abs/source/index.ts" - utilsPath := path.Join(path.Dir(indexPath), "lib", "utils.ts") - mockMultipartBody(t, project, slug, bundleMetadata{}, []multipartPart{ - {filename: indexPath, contents: "console.log('abs')"}, - {filename: utilsPath, contents: "export const util = 2;"}, - }) - - gock.New(utils.DefaultApiHost). - Get(fmt.Sprintf("/v1/projects/%s/functions/%s", project, slug)). - Reply(http.StatusOK). - JSON(api.FunctionSlugResponseOutput{ - Id: "1", - Name: slug, - Slug: slug, - Status: api.FunctionSlugResponseOutputStatus("ACTIVE"), - Version: 1, - CreatedAt: 0, - UpdatedAt: 0, - EntrypointPath: cast.Ptr("file://" + indexPath), - }) - - err := Run(context.Background(), slug, project, false, false, fsys) - require.NoError(t, err) - - root := filepath.Join(utils.FunctionsDir, slug) - data, err := afero.ReadFile(fsys, filepath.Join(root, "index.ts")) - require.NoError(t, err) - assert.Equal(t, "console.log('abs')", string(data)) - - data, err = afero.ReadFile(fsys, filepath.Join(root, "lib", "utils.ts")) - require.NoError(t, err) - assert.Equal(t, "export const util = 2;", string(data)) - - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("fails when response not multipart", func(t *testing.T) { - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - - defer gock.OffAll() - gock.New(utils.DefaultApiHost). - Get(fmt.Sprintf("/v1/projects/%s/functions/%s", project, slug)). - Reply(http.StatusOK). - JSON(api.FunctionSlugResponseOutput{ - Id: "1", - Name: slug, - Slug: slug, - Status: api.FunctionSlugResponseOutputStatus("ACTIVE"), - Version: 1, - CreatedAt: 0, - UpdatedAt: 0, - EntrypointPath: cast.Ptr(legacyEntrypointPath), - }) - - gock.New(utils.DefaultApiHost). - Get(fmt.Sprintf("/v1/projects/%s/functions/%s/body", project, slug)). - Reply(http.StatusOK). - SetHeader("Content-Type", "application/json"). - BodyString(`{"error":"no multipart"}`) - - err := Run(context.Background(), slug, project, false, false, fsys) - assert.ErrorContains(t, err, "expected multipart response") - }) - - t.Run("ignores unresolvable entrypoint path", func(t *testing.T) { - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - - defer gock.OffAll() - mockMultipartBody(t, project, slug, bundleMetadata{}, []multipartPart{ - {filename: "source/index.ts", contents: "console.log('hello')"}, - {filename: "source/secret.env", supabasePath: "../secret.env", contents: "SECRET=1"}, - }) - - gock.New(utils.DefaultApiHost). - Get(fmt.Sprintf("/v1/projects/%s/functions/%s", project, slug)). - Reply(http.StatusOK). - JSON(api.FunctionSlugResponseOutput{ - Id: "1", - Name: slug, - Slug: slug, - Status: api.FunctionSlugResponseOutputStatus("ACTIVE"), - Version: 1, - CreatedAt: 0, - UpdatedAt: 0, - EntrypointPath: cast.Ptr("file:///source/index.ts"), - }) - - err := Run(context.Background(), slug, project, false, false, fsys) - assert.NoError(t, err) - - root := filepath.Join(utils.FunctionsDir, slug) - data, err := afero.ReadFile(fsys, filepath.Join(root, "source", "index.ts")) - require.NoError(t, err) - assert.Equal(t, "console.log('hello')", string(data)) - - data, err = afero.ReadFile(fsys, filepath.Join(utils.FunctionsDir, "secret.env")) - require.NoError(t, err) - assert.Equal(t, "SECRET=1", string(data)) - - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("rejects a path traversal payload in Supabase-Path", func(t *testing.T) { - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - - utils.CmdSuggestion = "" - t.Cleanup(func() { utils.CmdSuggestion = "" }) - - defer gock.OffAll() - mockMultipartBody(t, project, slug, bundleMetadata{EntrypointPath: "source/index.ts"}, []multipartPart{ - {filename: "source/index.ts", contents: "console.log('hello')"}, - {filename: "leftover.ts", supabasePath: "../../../../../../etc/passwd", contents: "root:x:0:0::/root:/bin/bash"}, - }) - - err := Run(context.Background(), slug, project, false, false, fsys) - require.Error(t, err) - assert.ErrorIs(t, err, ErrUnsafeDownloadPath) - - // The generic "invalid path in server response" message on its own - // gives users nothing actionable, so this must carry a specific - // suggestion (DX finding on CLI-1891) rather than falling through - // to the generic --debug suggestion. - assert.Contains(t, utils.CmdSuggestion, "malformed or unexpected API response") - - // Nothing should have escaped the functions directory. - exists, err := afero.Exists(fsys, "/etc/passwd") - require.NoError(t, err) - assert.False(t, exists, "path traversal payload must not be written outside the functions directory") - - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("writes deeply nested legitimate paths", func(t *testing.T) { - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - - defer gock.OffAll() - mockMultipartBody(t, project, slug, bundleMetadata{EntrypointPath: "source/index.ts"}, []multipartPart{ - {filename: "source/index.ts", contents: "console.log('hello')"}, - {filename: "source/a/b/c/d/deep.ts", contents: "export const deep = true;"}, - }) - - err := Run(context.Background(), slug, project, false, false, fsys) - require.NoError(t, err) - - root := filepath.Join(utils.FunctionsDir, slug) - data, err := afero.ReadFile(fsys, filepath.Join(root, "index.ts")) - require.NoError(t, err) - assert.Equal(t, "console.log('hello')", string(data)) - - data, err = afero.ReadFile(fsys, filepath.Join(root, "a", "b", "c", "d", "deep.ts")) - require.NoError(t, err) - assert.Equal(t, "export const deep = true;", string(data)) - - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("does not write through a pre-existing symlink at the destination", func(t *testing.T) { - // Symlink semantics only exist on a real filesystem, so this test - // exercises afero.NewOsFs against an isolated temp directory rather - // than the in-memory fs used elsewhere in this file. - tmpDir := t.TempDir() - t.Chdir(tmpDir) - fsys := afero.NewOsFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - - // A file living outside the sandboxed functions directory that must - // never be touched by the download. - outsideDir := filepath.Join(tmpDir, "outside") - require.NoError(t, os.MkdirAll(outsideDir, 0o755)) - secretPath := filepath.Join(outsideDir, "secret.txt") - require.NoError(t, os.WriteFile(secretPath, []byte("original"), 0o600)) - - // Plant a symlink inside the function directory, before the - // download runs, pointing at the file outside the sandbox. A - // server response that resolves to this exact path must not be - // allowed to write through it. - funcDir := filepath.Join(utils.FunctionsDir, slug) - require.NoError(t, os.MkdirAll(funcDir, 0o755)) - linkPath := filepath.Join(funcDir, "evil.ts") - require.NoError(t, os.Symlink(secretPath, linkPath)) - - defer gock.OffAll() - mockMultipartBody(t, project, slug, bundleMetadata{EntrypointPath: "source/index.ts"}, []multipartPart{ - {filename: "source/index.ts", contents: "console.log('hello')"}, - {filename: "source/evil.ts", contents: "overwritten"}, - }) - - err := Run(context.Background(), slug, project, false, false, fsys) - require.NoError(t, err) - - // The symlink target outside the sandbox must be untouched. - data, err := os.ReadFile(secretPath) - require.NoError(t, err) - assert.Equal(t, "original", string(data), "file outside the functions directory must not be modified") - - // The destination itself should now be a plain file with the - // downloaded contents: the atomic rename replaces the symlink - // directory entry rather than following it. - info, err := os.Lstat(linkPath) - require.NoError(t, err) - assert.Zero(t, info.Mode()&os.ModeSymlink, "planted symlink should have been replaced, not written through") - - data, err = os.ReadFile(linkPath) - require.NoError(t, err) - assert.Equal(t, "overwritten", string(data)) - - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - // Regression test for the DX finding on CLI-1891: ensureNoSymlinkInPath - // used to reject the mere presence of a symlink anywhere under - // utils.FunctionsDir, which would also have broken a legitimate - // monorepo pattern like this one, where a function directory - // symlinks in a shared directory that itself lives inside the - // functions tree. That symlink's target still resolves inside root, - // so it must be allowed end-to-end. - t.Run("writes through a symlink pointing to a legitimate location inside the functions directory", func(t *testing.T) { - tmpDir := t.TempDir() - t.Chdir(tmpDir) - fsys := afero.NewOsFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - - sharedDir := filepath.Join(utils.FunctionsDir, "_shared") - require.NoError(t, os.MkdirAll(sharedDir, 0o755)) - // os.Symlink resolves a relative target against the symlink's own - // containing directory, not the process cwd, so the target must be - // made absolute here for the link to actually point at sharedDir. - absSharedDir, err := filepath.Abs(sharedDir) - require.NoError(t, err) - - funcDir := filepath.Join(utils.FunctionsDir, slug) - require.NoError(t, os.MkdirAll(funcDir, 0o755)) - require.NoError(t, os.Symlink(absSharedDir, filepath.Join(funcDir, "_shared"))) - - defer gock.OffAll() - mockMultipartBody(t, project, slug, bundleMetadata{EntrypointPath: "source/index.ts"}, []multipartPart{ - {filename: "source/index.ts", contents: "console.log('hello')"}, - {filename: "source/_shared/util.ts", contents: "export const util = 2;"}, - }) - - err = Run(context.Background(), slug, project, false, false, fsys) - require.NoError(t, err) - - data, err := afero.ReadFile(fsys, filepath.Join(funcDir, "index.ts")) - require.NoError(t, err) - assert.Equal(t, "console.log('hello')", string(data)) - - // The write landed through the symlink, in the real shared - // directory rather than being rejected. - data, err = os.ReadFile(filepath.Join(sharedDir, "util.ts")) - require.NoError(t, err) - assert.Equal(t, "export const util = 2;", string(data)) - - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} - -// TestEnsureNoSymlinkInPath exercises ensureNoSymlinkInPath's resolve-then-check -// policy directly: it must still reject a symlink whose target escapes root, -// but must now allow one whose target resolves to a legitimate, even deeply -// nested, location that is still inside root. -func TestEnsureNoSymlinkInPath(t *testing.T) { - t.Run("rejects a symlink whose target resolves outside root", func(t *testing.T) { - tmpDir := t.TempDir() - fsys := afero.NewOsFs() - - root := filepath.Join(tmpDir, "supabase", "functions") - require.NoError(t, os.MkdirAll(root, 0o755)) - - outsideDir := filepath.Join(tmpDir, "outside") - require.NoError(t, os.MkdirAll(outsideDir, 0o755)) - - // Plant a symlink inside root whose target lives outside it - // entirely -- the actual attack this check defends against. - linkDir := filepath.Join(root, "escape") - require.NoError(t, os.Symlink(outsideDir, linkDir)) - - dir := filepath.Join(linkDir, "nested") - err := ensureNoSymlinkInPath(fsys, root, dir) - require.Error(t, err) - assert.ErrorIs(t, err, ErrUnsafeDownloadPath) - }) - - t.Run("allows a symlink whose target resolves to a nested location inside root", func(t *testing.T) { - tmpDir := t.TempDir() - fsys := afero.NewOsFs() - - root := filepath.Join(tmpDir, "supabase", "functions") - require.NoError(t, os.MkdirAll(root, 0o755)) - - // A legitimate monorepo-style symlink: a shared directory that - // lives elsewhere inside the functions tree, symlinked into place - // from a function's own directory. - sharedDir := filepath.Join(root, "_shared") - require.NoError(t, os.MkdirAll(sharedDir, 0o755)) - - funcDir := filepath.Join(root, "my-func") - require.NoError(t, os.MkdirAll(funcDir, 0o755)) - linkDir := filepath.Join(funcDir, "_shared") - require.NoError(t, os.Symlink(sharedDir, linkDir)) - - // dir itself does not exist yet -- ensureNoSymlinkInPath is called - // before MkdirIfNotExistFS creates it -- so this also proves the - // still-nonexistent remainder is correctly re-joined onto the - // resolved ancestor. - dir := filepath.Join(linkDir, "nested", "deeper") - err := ensureNoSymlinkInPath(fsys, root, dir) - assert.NoError(t, err, "a symlink resolving to a legitimate location inside root must be allowed") - }) -} - -func TestDownloadFunction(t *testing.T) { - const slug = "test-func" - // Setup valid project ref - project := apitest.RandomProjectRef() - // Setup valid access token - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) - - t.Run("throws error on network error", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + project + "/functions/" + slug). - Reply(http.StatusOK). - JSON(api.FunctionResponseOutput{Id: "1"}) - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + project + "/functions/" + slug + "/body"). - ReplyError(errors.New("network error")) - // Run test - err := downloadFunction(context.Background(), project, slug, "") - // Check error - assert.ErrorContains(t, err, "network error") - }) - - t.Run("throws error on service unavailable", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + project + "/functions/" + slug). - Reply(http.StatusOK). - JSON(api.FunctionResponseOutput{Id: "1"}) - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + project + "/functions/" + slug + "/body"). - Reply(http.StatusServiceUnavailable) - // Run test - err := downloadFunction(context.Background(), project, slug, "") - // Check error - assert.ErrorContains(t, err, "Unexpected error downloading Function:") - }) - - t.Run("throws error on extract failure", func(t *testing.T) { - // Setup deno error - t.Setenv("TEST_DENO_ERROR", "extract failed") - // Setup mock api - defer gock.OffAll() - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + project + "/functions/" + slug). - Reply(http.StatusOK). - JSON(api.FunctionResponseOutput{Id: "1"}) - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + project + "/functions/" + slug + "/body"). - Reply(http.StatusOK) - // Run test - err := downloadFunction(context.Background(), project, slug, "") - // Check error - assert.ErrorContains(t, err, "Error downloading function: exit status 1\nextract failed\n") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} - -func TestGetMetadata(t *testing.T) { - const slug = "test-func" - project := apitest.RandomProjectRef() - // Setup valid access token - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) - - t.Run("fallback to default paths", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + project + "/functions/" + slug). - Reply(http.StatusOK). - JSON(api.FunctionResponseOutput{Id: "1"}) - // Run test - meta, err := getFunctionMetadata(context.Background(), project, slug) - // Check error - assert.NoError(t, err) - assert.Equal(t, legacyEntrypointPath, *meta.EntrypointPath) - assert.Equal(t, legacyImportMapPath, *meta.ImportMapPath) - }) - - t.Run("throws error on network error", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + project + "/functions/" + slug). - ReplyError(errors.New("network error")) - // Run test - meta, err := getFunctionMetadata(context.Background(), project, slug) - // Check error - assert.ErrorContains(t, err, "network error") - assert.Nil(t, meta) - }) - - t.Run("throws error on service unavailable", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + project + "/functions/" + slug). - Reply(http.StatusServiceUnavailable) - // Run test - meta, err := getFunctionMetadata(context.Background(), project, slug) - // Check error - assert.ErrorContains(t, err, "Failed to download Function test-func on the Supabase project:") - assert.Nil(t, meta) - }) -} - -func TestGetPartPath(t *testing.T) { - t.Parallel() - - t.Run("returns path from Supabase header", func(t *testing.T) { - header := textproto.MIMEHeader{} - header.Set("Supabase-Path", "dir/file.ts") - got, err := getPartPath(header) - require.NoError(t, err) - assert.Equal(t, "dir/file.ts", got) - }) - - t.Run("returns filename from content disposition", func(t *testing.T) { - header := textproto.MIMEHeader{} - header.Set("Content-Disposition", `form-data; name="file"; filename="test-func/index.ts"`) - got, err := getPartPath(header) - require.NoError(t, err) - assert.Equal(t, "test-func/index.ts", got) - }) - - t.Run("returns filename from editor-originated content disposition", func(t *testing.T) { - header := textproto.MIMEHeader{} - header.Set("Content-Disposition", `form-data; name="file"; filename="source/index.ts"`) - got, err := getPartPath(header) - require.NoError(t, err) - assert.Equal(t, "source/index.ts", got) - }) - - t.Run("writes file of arbitrary depth", func(t *testing.T) { - header := textproto.MIMEHeader{} - header.Set("Content-Disposition", `form-data; name="file"; filename="test-func/dir/subdir/file.ts"`) - got, err := getPartPath(header) - require.NoError(t, err) - assert.Equal(t, "test-func/dir/subdir/file.ts", got) - }) - - t.Run("returns empty when no filename provided", func(t *testing.T) { - header := textproto.MIMEHeader{} - header.Set("Content-Disposition", `form-data; name="file"`) - got, err := getPartPath(header) - require.NoError(t, err) - assert.Equal(t, "", got) - }) - - t.Run("returns error on invalid content disposition", func(t *testing.T) { - header := textproto.MIMEHeader{} - header.Set("Content-Disposition", `form-data; filename="unterminated`) - got, err := getPartPath(header) - require.ErrorContains(t, err, "failed to parse content disposition") - assert.Equal(t, "", got) - }) -} diff --git a/apps/cli-go/internal/gen/types/templates/prod-ca-2021.crt b/apps/cli-go/internal/gen/types/templates/prod-ca-2021.crt deleted file mode 100644 index 3d693669b2..0000000000 --- a/apps/cli-go/internal/gen/types/templates/prod-ca-2021.crt +++ /dev/null @@ -1,23 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDxDCCAqygAwIBAgIUbLxMod62P2ktCiAkxnKJwtE9VPYwDQYJKoZIhvcNAQEL -BQAwazELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0RlbHdhcmUxEzARBgNVBAcMCk5l -dyBDYXN0bGUxFTATBgNVBAoMDFN1cGFiYXNlIEluYzEeMBwGA1UEAwwVU3VwYWJh -c2UgUm9vdCAyMDIxIENBMB4XDTIxMDQyODEwNTY1M1oXDTMxMDQyNjEwNTY1M1ow -azELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0RlbHdhcmUxEzARBgNVBAcMCk5ldyBD -YXN0bGUxFTATBgNVBAoMDFN1cGFiYXNlIEluYzEeMBwGA1UEAwwVU3VwYWJhc2Ug -Um9vdCAyMDIxIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQXW -QyHOB+qR2GJobCq/CBmQ40G0oDmCC3mzVnn8sv4XNeWtE5XcEL0uVih7Jo4Dkx1Q -DmGHBH1zDfgs2qXiLb6xpw/CKQPypZW1JssOTMIfQppNQ87K75Ya0p25Y3ePS2t2 -GtvHxNjUV6kjOZjEn2yWEcBdpOVCUYBVFBNMB4YBHkNRDa/+S4uywAoaTWnCJLUi -cvTlHmMw6xSQQn1UfRQHk50DMCEJ7Cy1RxrZJrkXXRP3LqQL2ijJ6F4yMfh+Gyb4 -O4XajoVj/+R4GwywKYrrS8PrSNtwxr5StlQO8zIQUSMiq26wM8mgELFlS/32Uclt -NaQ1xBRizkzpZct9DwIDAQABo2AwXjALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFKjX -uXY32CztkhImng4yJNUtaUYsMB8GA1UdIwQYMBaAFKjXuXY32CztkhImng4yJNUt -aUYsMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAB8spzNn+4VU -tVxbdMaX+39Z50sc7uATmus16jmmHjhIHz+l/9GlJ5KqAMOx26mPZgfzG7oneL2b -VW+WgYUkTT3XEPFWnTp2RJwQao8/tYPXWEJDc0WVQHrpmnWOFKU/d3MqBgBm5y+6 -jB81TU/RG2rVerPDWP+1MMcNNy0491CTL5XQZ7JfDJJ9CCmXSdtTl4uUQnSuv/Qx -Cea13BX2ZgJc7Au30vihLhub52De4P/4gonKsNHYdbWjg7OWKwNv/zitGDVDB9Y2 -CMTyZKG3XEu5Ghl1LEnI3QmEKsqaCLv12BnVjbkSeZsMnevJPs1Ye6TjjJwdik5P -o/bKiIz+Fq8= ------END CERTIFICATE----- diff --git a/apps/cli-go/internal/gen/types/templates/prod-ca-2025.crt b/apps/cli-go/internal/gen/types/templates/prod-ca-2025.crt deleted file mode 100644 index da35f1f84a..0000000000 --- a/apps/cli-go/internal/gen/types/templates/prod-ca-2025.crt +++ /dev/null @@ -1,23 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDxzCCAq+gAwIBAgIUeX+gpfmsRW9asFkRvjyXjHxbfgcwDQYJKoZIhvcNAQEL -BQAwazELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0RlbHdhcmUxEzARBgNVBAcMCk5l -dyBDYXN0bGUxFTATBgNVBAoMDFN1cGFiYXNlIEluYzEeMBwGA1UEAwwVU3VwYWJh -c2UgUm9vdCAyMDIxIENBMB4XDTI1MDkwMzA4MDEyNVoXDTM1MDkwMTA4MDEyNVow -azELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0RlbHdhcmUxEzARBgNVBAcMCk5ldyBD -YXN0bGUxFTATBgNVBAoMDFN1cGFiYXNlIEluYzEeMBwGA1UEAwwVU3VwYWJhc2Ug -Um9vdCAyMDIxIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5Ve7 -i9UAmc7luUilELPtqzEk8nGHxg7nY0aCStr625M7+K4OPO6RUllTsHh47k1jWyzm -LXLlyYwCsYCjQp+3vn06H+F/HRUxBt6CK2B7bNng230exTunk0xFvfkX6YgHR7B3 -1B7L25Rq3PhuRFPV4hnGYRam2XBZC4UNPqoAgrhV0HOYzXXAVoTr2yaBTMnB331Z -RwOmINh7eqTCk/JRZbb6vfZOhZRAVAe9AoRLoG8aKwmeoLGwlu0UuFx6z3E+6bmA -fSNa8Lx02GEoCdPLw9IRKUFq/SgBpQUKm44H1fDwTjH2CMM0N4p0mL/6wXnNeHvt -C40MmKZ0RcVmHE5wBwIDAQABo2MwYTAdBgNVHQ4EFgQUjvEE541toZcwtXQlZlcB -YOBRTnowHwYDVR0jBBgwFoAUjvEE541toZcwtXQlZlcBYOBRTnowDwYDVR0TAQH/ -BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggEBACD5IcGP -XKvS9qg0CgEQPFqYavt5c7P+0xxFgiZe+xoG8fUw58yNeK2APtgGPRpxEOGfAlNx -z9HDt4gcyHEE00B3qAVDm49pqNxioFWzNqU2LGfM/HL1QmN6urR7hCOkVCJddvOc -FhFX4nZDuRfaBboDvS5HlK3Pzxddp9hvrJi2bemr8HLqYc3HzmVckgPGSLML6t+h -4LRCXSlQsDgQ1LZ4KHsl4cq7K51N6FOXQBLB5q4lMKhs0VUhCT8Pdsj12+84laCV -c22q6p2mdT9SaernCSRnWazXWisgpjv3H7Ex4S1DCYjJIwn3PUToGFv1r8YRN2/S -O19yVSxxCIf64Sg= ------END CERTIFICATE----- diff --git a/apps/cli-go/internal/gen/types/templates/staging-ca-2021.crt b/apps/cli-go/internal/gen/types/templates/staging-ca-2021.crt deleted file mode 100644 index 925f9f5af0..0000000000 --- a/apps/cli-go/internal/gen/types/templates/staging-ca-2021.crt +++ /dev/null @@ -1,23 +0,0 @@ ------BEGIN CERTIFICATE----- -MIID1DCCArygAwIBAgIUbYRdq/8/uNq8G9stMCdOFSBgA2MwDQYJKoZIhvcNAQEL -BQAwczELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0RlbHdhcmUxEzARBgNVBAcMCk5l -dyBDYXN0bGUxFTATBgNVBAoMDFN1cGFiYXNlIEluYzEmMCQGA1UEAwwdU3VwYWJh -c2UgU3RhZ2luZyBSb290IDIwMjEgQ0EwHhcNMjEwNDI4MTAzNjEzWhcNMzEwNDI2 -MTAzNjEzWjBzMQswCQYDVQQGEwJVUzEQMA4GA1UECAwHRGVsd2FyZTETMBEGA1UE -BwwKTmV3IENhc3RsZTEVMBMGA1UECgwMU3VwYWJhc2UgSW5jMSYwJAYDVQQDDB1T -dXBhYmFzZSBTdGFnaW5nIFJvb3QgMjAyMSBDQTCCASIwDQYJKoZIhvcNAQEBBQAD -ggEPADCCAQoCggEBAN0AKRE8a56O8LaZxiOAcHFUFnwiKUvPoXPq26Ifw+Nv+7zg -N2V5WnMZbbw24q61Os60ZUn0XmbVtuIeJ+stPHsO7qxxuL+bmPR+qU5tkDrIOyEe -YD/2u8/q6ssVv42k4XcXbhM6RVz7CkCDY0TiBm1bMtRZso3xB6E9wAjxDf43XfV5 -PAGs3JI+Zo/vyqCDlN0hHOrB/aBl01JXqQWI84Gia5ooucq4SjA1CyawBcQ2IAvG -rXuy1BouY+xM3zRuNvtfFP6rb5Mta+jCYEMh1AZ8yP8sYUWAyhxX6k9EbOb009wQ -aZljbUCh/UglGWuBxdzePavx+zPjzWXB1NyVkpkCAwEAAaNgMF4wCwYDVR0PBAQD -AgEGMB0GA1UdDgQWBBQFx+PHLf27iIo/PMfIfGqXF7Zb+DAfBgNVHSMEGDAWgBQF -x+PHLf27iIo/PMfIfGqXF7Zb+DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB -CwUAA4IBAQB/xIiz5dDqzGXjqYqXZYx4iSfSxsVayeOPDMfmaiCfSMJEUG4cUiwG -OvMPGztaUEYeip5SCvSKuAAjVkXyP7ahKR7t7lZ9mErVXyxSZoVLbOd578CuYiZk -OgT17UjPv66WMzEKEr8wGpomTYWWfEkuqt8ENdiM1Z4LNFahdKj36+jm6/a+9R8K -25VIL68DTaQpBxFWG6ixC1HRMHJ12lDhKsshIi099BVpkGibESlxPrQOdKKqBB/J -vIX+/Hb+mS4H5zYMeK2wX0onp+GBcD6X9L1UJuXMVd+BRan8RFidXL5s3++xXjQq -Nzbc6lnA69urKffvcT07YwMsY/OmHzVa ------END CERTIFICATE----- diff --git a/apps/cli-go/internal/gen/types/types.go b/apps/cli-go/internal/gen/types/types.go deleted file mode 100644 index 688ab6302f..0000000000 --- a/apps/cli-go/internal/gen/types/types.go +++ /dev/null @@ -1,253 +0,0 @@ -package types - -import ( - "context" - _ "embed" - "fmt" - "net/url" - "os" - "runtime" - "strings" - "time" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/network" - "github.com/go-errors/errors" - "github.com/jackc/pgconn" - "github.com/jackc/pgx/v4" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/api" -) - -const ( - LangTypescript = "typescript" - LangGo = "go" - LangSwift = "swift" - LangPython = "python" -) - -const ( - SwiftPublicAccessControl = "public" - SwiftInternalAccessControl = "internal" -) - -func Run(ctx context.Context, projectId string, dbConfig pgconn.Config, lang string, schemas []string, postgrestV9Compat bool, swiftAccessControl string, queryTimeout time.Duration, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { - originalURL := utils.ToPostgresURL(dbConfig) - // Add default schemas if --schema flag is not specified - if len(schemas) == 0 { - schemas = utils.RemoveDuplicates(append([]string{"public"}, utils.Config.Api.Schemas...)) - } - included := strings.Join(schemas, ",") - - if projectId != "" { - if lang != LangTypescript { - return errors.Errorf("Unable to generate %s types for selected project. Try using --db-url flag instead.", lang) - } - resp, err := utils.GetSupabase().V1GenerateTypescriptTypesWithResponse(ctx, projectId, &api.V1GenerateTypescriptTypesParams{ - IncludedSchemas: &included, - }) - if err != nil { - return errors.Errorf("failed to get typescript types: %w", err) - } - - if resp.JSON200 == nil { - return errors.New("failed to retrieve generated types: " + string(resp.Body)) - } - - fmt.Print(resp.JSON200.Types) - return nil - } - - hostConfig := container.HostConfig{} - if utils.IsLocalDatabase(dbConfig) { - if err := utils.AssertSupabaseDbIsRunning(); err != nil { - return err - } - - if strings.Contains(utils.Config.Api.Image, "v9") { - postgrestV9Compat = true - } - - // Use custom network when connecting to local database - dbConfig.Host = utils.DbAliases[0] - dbConfig.Port = 5432 - } else { - hostConfig.NetworkMode = network.NetworkHost - } - // pg-meta does not set username as the default database, ie. postgres - if len(dbConfig.Database) == 0 { - dbConfig.Database = "postgres" - } - - fmt.Fprintln(os.Stderr, "Connecting to", dbConfig.Host, dbConfig.Port) - env := []string{ - "PG_META_DB_URL=" + utils.ToPostgresURL(dbConfig), - fmt.Sprintf("PG_CONN_TIMEOUT_SECS=%.0f", queryTimeout.Seconds()), - fmt.Sprintf("PG_QUERY_TIMEOUT_SECS=%.0f", queryTimeout.Seconds()), - "PG_META_GENERATE_TYPES=" + lang, - "PG_META_GENERATE_TYPES_INCLUDED_SCHEMAS=" + included, - "PG_META_GENERATE_TYPES_SWIFT_ACCESS_CONTROL=" + swiftAccessControl, - fmt.Sprintf("PG_META_GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS=%v", !postgrestV9Compat), - } - if ca, err := GetRootCA(ctx, originalURL, options...); err != nil { - return err - } else if len(ca) > 0 { - env = append(env, "PG_META_DB_SSL_ROOT_CERT="+ca) - } - - return utils.DockerRunOnceWithConfig( - ctx, - container.Config{ - Image: utils.Config.Studio.PgmetaImage, - Env: env, - Cmd: []string{"node", "dist/server/server.js"}, - }, - hostConfig, - network.NetworkingConfig{}, - "", - os.Stdout, - os.Stderr, - ) -} - -var ( - //go:embed templates/staging-ca-2021.crt - caStaging string - //go:embed templates/prod-ca-2021.crt - caProd string - //go:embed templates/prod-ca-2025.crt - caSnap string -) - -func GetRootCA(ctx context.Context, dbURL string, options ...func(*pgx.ConnConfig)) (string, error) { - debugf := func(string, ...any) {} - if IsSSLDebugEnabled() { - debugf = LogSSLDebugf - } - debugf("GetRootCA start db_url=%s", redactPostgresURL(dbURL)) - debugf("env SUPABASE_CA_SKIP_VERIFY=%q SUPABASE_SSL_DEBUG=%q PGSSLROOTCERT=%q SSL_CERT_FILE=%q SSL_CERT_DIR=%q", - os.Getenv("SUPABASE_CA_SKIP_VERIFY"), - os.Getenv("SUPABASE_SSL_DEBUG"), - os.Getenv("PGSSLROOTCERT"), - os.Getenv("SSL_CERT_FILE"), - os.Getenv("SSL_CERT_DIR"), - ) - debugf("runtime goos=%s goarch=%s go=%s", runtime.GOOS, runtime.GOARCH, runtime.Version()) - // node-postgres does not support sslmode=prefer - require, err := isRequireSSL(ctx, dbURL, options...) - debugf("GetRootCA probe_result require_ssl=%t err=%v", require, err) - if !require { - return "", err - } - // Merge all certs to support --db-url flag - ca := caStaging + caProd + caSnap - debugf("GetRootCA return ca_bundle_len=%d", len(ca)) - return ca, nil -} - -func isRequireSSL(ctx context.Context, dbUrl string, options ...func(*pgx.ConnConfig)) (bool, error) { - debugf := func(string, ...any) {} - if IsSSLDebugEnabled() { - debugf = LogSSLDebugf - } - // pgx v4's sslmode=require verifies the server certificate against system CAs, - // unlike libpq where require skips verification. When SUPABASE_CA_SKIP_VERIFY=true, - // skip verification for this probe only (detects whether the server speaks TLS). - // pgconn may still install VerifyPeerCertificate callback when sslrootcert is set, - // so we also clear custom verification callbacks on all TLS configs. - // Cert validation happens downstream in the migra/pgdelta Deno scripts using GetRootCA. - opts := append([]func(*pgx.ConnConfig){}, options...) - if os.Getenv("SUPABASE_CA_SKIP_VERIFY") == "true" { - fmt.Fprintln(os.Stderr, "WARNING: TLS certificate verification disabled for SSL probe (SUPABASE_CA_SKIP_VERIFY=true)") - opts = append(opts, func(cc *pgx.ConnConfig) { - // #nosec G402 -- Intentionally skipped for this TLS capability probe only. - // Downstream migra/pgdelta flows still validate certificates using GetRootCA. - if cc.TLSConfig != nil { - cc.TLSConfig.InsecureSkipVerify = true - cc.TLSConfig.VerifyPeerCertificate = nil - cc.TLSConfig.VerifyConnection = nil - } - for _, fc := range cc.Fallbacks { - if fc.TLSConfig == nil { - continue - } - fc.TLSConfig.InsecureSkipVerify = true - fc.TLSConfig.VerifyPeerCertificate = nil - fc.TLSConfig.VerifyConnection = nil - } - }) - } - debugf("isRequireSSL probe db_url=%s skip_verify=%t", redactPostgresURL(dbUrl), os.Getenv("SUPABASE_CA_SKIP_VERIFY") == "true") - if IsSSLDebugEnabled() { - opts = append(opts, logTLSConfigState("isRequireSSL", dbUrl)) - } - conn, err := utils.ConnectByUrl(ctx, dbUrl+"&sslmode=require", opts...) - if err != nil { - debugf("isRequireSSL probe_error err=%v", err) - if strings.HasSuffix(err.Error(), "(server refused TLS connection)") { - debugf("isRequireSSL result require_ssl=false reason=server_refused_tls") - return false, nil - } - return false, err - } - debugf("isRequireSSL result require_ssl=true") - return true, conn.Close(ctx) -} - -func IsSSLDebugEnabled() bool { - return strings.EqualFold(os.Getenv("SUPABASE_SSL_DEBUG"), "true") -} - -func LogSSLDebugf(format string, args ...any) { - fmt.Fprintf(os.Stderr, "[ssl-debug] "+format+"\n", args...) -} - -func redactPostgresURL(raw string) string { - parsed, err := url.Parse(raw) - if err != nil { - return "" - } - if parsed.User != nil { - username := parsed.User.Username() - if username == "" { - parsed.User = url.UserPassword("redacted", "xxxxx") - } else { - parsed.User = url.UserPassword(username, "xxxxx") - } - } - return parsed.String() -} - -func logTLSConfigState(scope, dbUrl string) func(*pgx.ConnConfig) { - return func(cc *pgx.ConnConfig) { - if cc.TLSConfig == nil { - LogSSLDebugf("%s tls_config=nil db_url=%s fallbacks=%d", scope, redactPostgresURL(dbUrl), len(cc.Fallbacks)) - return - } - LogSSLDebugf("%s tls_config skip_verify=%t verify_peer_cb=%t verify_conn_cb=%t root_cas=%t server_name=%q fallbacks=%d", - scope, - cc.TLSConfig.InsecureSkipVerify, - cc.TLSConfig.VerifyPeerCertificate != nil, - cc.TLSConfig.VerifyConnection != nil, - cc.TLSConfig.RootCAs != nil, - cc.TLSConfig.ServerName, - len(cc.Fallbacks), - ) - for i, fc := range cc.Fallbacks { - if fc == nil || fc.TLSConfig == nil { - LogSSLDebugf("%s fallback[%d] tls_config=nil", scope, i) - continue - } - LogSSLDebugf("%s fallback[%d] skip_verify=%t verify_peer_cb=%t verify_conn_cb=%t root_cas=%t server_name=%q", - scope, - i, - fc.TLSConfig.InsecureSkipVerify, - fc.TLSConfig.VerifyPeerCertificate != nil, - fc.TLSConfig.VerifyConnection != nil, - fc.TLSConfig.RootCAs != nil, - fc.TLSConfig.ServerName, - ) - } - } -} diff --git a/apps/cli-go/internal/gen/types/types_test.go b/apps/cli-go/internal/gen/types/types_test.go deleted file mode 100644 index 915f85ad84..0000000000 --- a/apps/cli-go/internal/gen/types/types_test.go +++ /dev/null @@ -1,192 +0,0 @@ -package types - -import ( - "context" - "errors" - "net/http" - "testing" - "time" - - "github.com/docker/docker/api/types/container" - "github.com/h2non/gock" - "github.com/jackc/pgconn" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/testing/apitest" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/pgtest" -) - -func TestGenLocalCommand(t *testing.T) { - utils.DbId = "test-db" - utils.Config.Hostname = "localhost" - utils.Config.Db.Port = 5432 - - dbConfig := pgconn.Config{ - Host: utils.Config.Hostname, - Port: utils.Config.Db.Port, - User: "admin", - Password: "password", - } - - t.Run("generates typescript types", func(t *testing.T) { - const containerId = "test-pgmeta" - imageUrl := utils.GetRegistryImageUrl(utils.Config.Studio.PgmetaImage) - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId). - Reply(http.StatusOK). - JSON(container.InspectResponse{}) - apitest.MockDockerStart(utils.Docker, imageUrl, containerId) - require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "hello world\n")) - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - // Run test - assert.NoError(t, Run(context.Background(), "", dbConfig, LangTypescript, []string{}, true, "", time.Second, fsys, conn.Intercept)) - // Validate api - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error when db is not started", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId). - Reply(http.StatusServiceUnavailable) - // Run test - assert.Error(t, Run(context.Background(), "", dbConfig, LangTypescript, []string{}, true, "", time.Second, fsys)) - // Validate api - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on image fetch failure", func(t *testing.T) { - utils.Config.Api.Image = "v9" - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId). - Reply(http.StatusOK). - JSON(container.InspectResponse{}) - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/images"). - Reply(http.StatusServiceUnavailable) - // Run test - assert.Error(t, Run(context.Background(), "", dbConfig, LangTypescript, []string{}, true, "", time.Second, fsys)) - // Validate api - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("generates swift types", func(t *testing.T) { - const containerId = "test-pgmeta" - imageUrl := utils.GetRegistryImageUrl(utils.Config.Studio.PgmetaImage) - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId). - Reply(http.StatusOK). - JSON(container.InspectResponse{}) - apitest.MockDockerStart(utils.Docker, imageUrl, containerId) - require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "hello world\n")) - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - // Run test - assert.NoError(t, Run(context.Background(), "", dbConfig, LangSwift, []string{}, true, SwiftInternalAccessControl, time.Second, fsys, conn.Intercept)) - // Validate api - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} - -func TestGenLinkedCommand(t *testing.T) { - // Setup valid projectId id - projectId := apitest.RandomProjectRef() - // Setup valid access token - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) - - t.Run("generates typescript types", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Flush pending mocks after test execution - defer gock.OffAll() - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + projectId + "/types/typescript"). - Reply(200). - JSON(api.TypescriptResponseOutput{Types: ""}) - // Run test - assert.NoError(t, Run(context.Background(), projectId, pgconn.Config{}, LangTypescript, []string{}, true, "", time.Second, fsys)) - // Validate api - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on network failure", func(t *testing.T) { - errNetwork := errors.New("network error") - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Flush pending mocks after test execution - defer gock.OffAll() - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + projectId + "/types/typescript"). - ReplyError(errNetwork) - // Run test - err := Run(context.Background(), projectId, pgconn.Config{}, LangTypescript, []string{}, true, "", time.Second, fsys) - // Validate api - assert.ErrorIs(t, err, errNetwork) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on service unavailable", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Flush pending mocks after test execution - defer gock.OffAll() - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + projectId + "/types/typescript"). - Reply(http.StatusServiceUnavailable) - // Run test - assert.Error(t, Run(context.Background(), projectId, pgconn.Config{}, LangTypescript, []string{}, true, "", time.Second, fsys)) - }) -} - -func TestGenRemoteCommand(t *testing.T) { - dbConfig := pgconn.Config{ - Host: "db.supabase.co", - Port: 5432, - User: "admin", - Password: "password", - Database: "postgres", - } - - t.Run("generates type from remote db", func(t *testing.T) { - const containerId = "test-pgmeta" - imageUrl := utils.GetRegistryImageUrl(utils.Config.Studio.PgmetaImage) - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - apitest.MockDockerStart(utils.Docker, imageUrl, containerId) - require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "hello world\n")) - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - // Run test - assert.NoError(t, Run(context.Background(), "", dbConfig, LangTypescript, []string{"public"}, true, "", time.Second, afero.NewMemMapFs(), conn.Intercept)) - // Validate api - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} diff --git a/apps/cli-go/internal/migration/apply/apply.go b/apps/cli-go/internal/migration/apply/apply.go deleted file mode 100644 index 18bd330ea6..0000000000 --- a/apps/cli-go/internal/migration/apply/apply.go +++ /dev/null @@ -1,68 +0,0 @@ -package apply - -import ( - "context" - "fmt" - "io/fs" - - "github.com/jackc/pgx/v4" - "github.com/spf13/afero" - "github.com/spf13/viper" - "github.com/supabase/cli/internal/migration/list" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/migration" -) - -func MigrateAndSeed(ctx context.Context, version string, conn *pgx.Conn, fsys afero.Fs) error { - // If pg-delta is enabled, the source of truth for migrations is always the migrations only or the declarative files. - // Declarative files must be used with `db schema declarative sync` commands always. - if viper.GetBool("EXPERIMENTAL") && len(version) == 0 && !utils.IsPgDeltaEnabled() { - if err := applySchemaFiles(ctx, conn, afero.NewIOFS(fsys)); err != nil { - return err - } - } else if err := applyMigrationFiles(ctx, version, conn, fsys); err != nil { - return err - } - return applySeedFiles(ctx, conn, fsys) -} - -func applyMigrationFiles(ctx context.Context, version string, conn *pgx.Conn, fsys afero.Fs) error { - if !utils.Config.Db.Migrations.Enabled { - return nil - } - migrations, err := list.LoadPartialMigrations(version, fsys) - if err != nil { - return err - } - return migration.ApplyMigrations(ctx, migrations, conn, afero.NewIOFS(fsys)) -} - -func applySeedFiles(ctx context.Context, conn *pgx.Conn, fsys afero.Fs) error { - if !utils.Config.Db.Seed.Enabled { - return nil - } - seeds, err := migration.GetPendingSeeds(ctx, utils.Config.Db.Seed.SqlPaths, conn, afero.NewIOFS(fsys)) - if err != nil { - return err - } - return migration.SeedData(ctx, seeds, conn, afero.NewIOFS(fsys)) -} - -func applySchemaFiles(ctx context.Context, conn *pgx.Conn, fsys fs.FS) error { - declared, err := utils.Config.Db.Migrations.SchemaPaths.SQLFiles(fsys) - if len(declared) == 0 { - return err - } - for _, fp := range declared { - schema, err := migration.NewMigrationFromFile(fp, fsys) - if err != nil { - return err - } - schema.Version = "" - if err := schema.ExecBatch(ctx, conn); err != nil { - utils.CmdSuggestion = fmt.Sprintf("See schema file: %s", utils.Bold(fp)) - return err - } - } - return nil -} diff --git a/apps/cli-go/internal/migration/apply/apply_test.go b/apps/cli-go/internal/migration/apply/apply_test.go deleted file mode 100644 index f45891cfb4..0000000000 --- a/apps/cli-go/internal/migration/apply/apply_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package apply - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/testing/fstest" - "github.com/supabase/cli/internal/testing/helper" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/migration" - "github.com/supabase/cli/pkg/pgtest" -) - -func TestMigrateDatabase(t *testing.T) { - t.Run("applies local migration", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - path := filepath.Join(utils.MigrationsDir, "0_test.sql") - sql := "create schema public" - require.NoError(t, afero.WriteFile(fsys, path, []byte(sql), 0644)) - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - helper.MockMigrationHistory(conn). - Query("RESET ALL"). - Reply("RESET"). - Query(sql). - Reply("CREATE SCHEMA"). - Query(migration.INSERT_MIGRATION_VERSION, "0", "test", []string{sql}). - Reply("INSERT 0 1") - // Run test - err := MigrateAndSeed(context.Background(), "", conn.MockClient(t), fsys) - // Check error - assert.NoError(t, err) - }) - - t.Run("skip seeding when seed config is disabled", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - path := filepath.Join(utils.MigrationsDir, "0_test.sql") - sql := "create schema public" - require.NoError(t, afero.WriteFile(fsys, path, []byte(sql), 0644)) - seedPath := filepath.Join(utils.SupabaseDirPath, "seed.sql") - // This will raise an error when seeding - require.NoError(t, afero.WriteFile(fsys, seedPath, []byte("INSERT INTO test_table;"), 0644)) - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - helper.MockMigrationHistory(conn). - Query("RESET ALL"). - Reply("RESET"). - Query(sql). - Reply("CREATE SCHEMA"). - Query(migration.INSERT_MIGRATION_VERSION, "0", "test", []string{sql}). - Reply("INSERT 0 1") - utils.Config.Db.Seed.Enabled = false - // Run test - err := MigrateAndSeed(context.Background(), "", conn.MockClient(t), fsys) - // No error should be returned since seeding is skipped - assert.NoError(t, err) - }) - - t.Run("ignores empty local directory", func(t *testing.T) { - assert.NoError(t, MigrateAndSeed(context.Background(), "", nil, afero.NewMemMapFs())) - }) - - t.Run("throws error on open failure", func(t *testing.T) { - // Setup in-memory fs - fsys := &fstest.OpenErrorFs{DenyPath: utils.MigrationsDir} - // Run test - err := MigrateAndSeed(context.Background(), "", nil, fsys) - // Check error - assert.ErrorIs(t, err, os.ErrPermission) - }) -} diff --git a/apps/cli-go/internal/migration/down/down.go b/apps/cli-go/internal/migration/down/down.go deleted file mode 100644 index 35a9fa7e88..0000000000 --- a/apps/cli-go/internal/migration/down/down.go +++ /dev/null @@ -1,64 +0,0 @@ -package down - -import ( - "context" - "fmt" - "os" - - "github.com/go-errors/errors" - "github.com/jackc/pgconn" - "github.com/jackc/pgx/v4" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/migration/apply" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/migration" - "github.com/supabase/cli/pkg/vault" -) - -func Run(ctx context.Context, last uint, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { - if last == 0 { - return errors.Errorf("--last must be greater than 0") - } - conn, err := utils.ConnectByConfig(ctx, config, options...) - if err != nil { - return err - } - defer conn.Close(context.Background()) - remoteMigrations, err := migration.ListRemoteMigrations(ctx, conn) - if err != nil { - return err - } - total := uint(len(remoteMigrations)) - if total <= last { - utils.CmdSuggestion = fmt.Sprintf("Try %s if you want to revert all migrations.", utils.Aqua("supabase db reset")) - return errors.Errorf("--last must be smaller than total applied migrations: %d", total) - } - msg := confirmResetAll(remoteMigrations[total-last:]) - if shouldReset, err := utils.NewConsole().PromptYesNo(ctx, msg, false); err != nil { - return err - } else if !shouldReset { - return errors.New(context.Canceled) - } - version := remoteMigrations[total-last-1] - fmt.Fprintln(os.Stderr, "Resetting database to version:", version) - return ResetAll(ctx, version, conn, fsys) -} - -func ResetAll(ctx context.Context, version string, conn *pgx.Conn, fsys afero.Fs) error { - if err := migration.DropUserSchemas(ctx, conn); err != nil { - return err - } - if err := vault.UpsertVaultSecrets(ctx, utils.Config.Db.Vault, conn); err != nil { - return err - } - return apply.MigrateAndSeed(ctx, version, conn, fsys) -} - -func confirmResetAll(pending []string) string { - msg := fmt.Sprintln("Do you want to revert the following migrations?") - for _, v := range pending { - msg += fmt.Sprintf(" • %s\n", utils.Bold(v)) - } - msg += fmt.Sprintf("%s you will lose all data in this database.", utils.Yellow("WARNING:")) - return msg -} diff --git a/apps/cli-go/internal/migration/down/down_test.go b/apps/cli-go/internal/migration/down/down_test.go deleted file mode 100644 index 89d9ff66d4..0000000000 --- a/apps/cli-go/internal/migration/down/down_test.go +++ /dev/null @@ -1,139 +0,0 @@ -package down - -import ( - "context" - "path/filepath" - "testing" - - "github.com/jackc/pgconn" - "github.com/jackc/pgerrcode" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/testing/helper" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/migration" - "github.com/supabase/cli/pkg/pgtest" -) - -var dbConfig = pgconn.Config{ - Host: "127.0.0.1", - Port: 5432, - User: "admin", - Password: "password", - Database: "postgres", -} - -func TestMigrationsDown(t *testing.T) { - t.Run("resets last n migrations", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - files := []string{ - filepath.Join(utils.MigrationsDir, "20221201000000_test.sql"), - filepath.Join(utils.MigrationsDir, "20221201000001_test.sql"), - filepath.Join(utils.MigrationsDir, "20221201000002_test.sql"), - } - for _, path := range files { - require.NoError(t, afero.WriteFile(fsys, path, []byte(""), 0644)) - } - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(migration.LIST_MIGRATION_VERSION). - Reply("SELECT 2", []any{"20221201000000"}, []any{"20221201000001"}) - // Run test - err := Run(context.Background(), 1, dbConfig, fsys, conn.Intercept) - // Check error - assert.ErrorIs(t, err, context.Canceled) - }) - - t.Run("throws error on out of range", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(migration.LIST_MIGRATION_VERSION). - Reply("SELECT 2", []any{"20221201000000"}, []any{"20221201000001"}) - // Run test - err := Run(context.Background(), 2, dbConfig, fsys, conn.Intercept) - // Check error - assert.ErrorContains(t, err, "--last must be smaller than total applied migrations: 2") - }) - - t.Run("throws error on insufficient privilege", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(migration.LIST_MIGRATION_VERSION). - ReplyError(pgerrcode.InsufficientPrivilege, "permission denied for relation supabase_migrations") - // Run test - err := Run(context.Background(), 1, dbConfig, fsys, conn.Intercept) - // Check error - assert.ErrorContains(t, err, "ERROR: permission denied for relation supabase_migrations (SQLSTATE 42501)") - }) -} - -func TestResetRemote(t *testing.T) { - t.Run("resets remote database", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - path := filepath.Join(utils.MigrationsDir, "0_schema.sql") - require.NoError(t, afero.WriteFile(fsys, path, nil, 0644)) - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(migration.DropObjects). - Reply("INSERT 0") - helper.MockMigrationHistory(conn). - Query("RESET ALL"). - Reply("RESET"). - Query(migration.INSERT_MIGRATION_VERSION, "0", "schema", nil). - Reply("INSERT 0 1") - // Run test - err := ResetAll(context.Background(), "", conn.MockClient(t), fsys) - // Check error - assert.NoError(t, err) - }) - - t.Run("resets remote database with seed config disabled", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - path := filepath.Join(utils.MigrationsDir, "0_schema.sql") - require.NoError(t, afero.WriteFile(fsys, path, nil, 0644)) - seedPath := filepath.Join(utils.SupabaseDirPath, "seed.sql") - // Will raise an error when seeding - require.NoError(t, afero.WriteFile(fsys, seedPath, []byte("INSERT INTO test_table;"), 0644)) - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(migration.DropObjects). - Reply("INSERT 0") - helper.MockMigrationHistory(conn). - Query("RESET ALL"). - Reply("RESET"). - Query(migration.INSERT_MIGRATION_VERSION, "0", "schema", nil). - Reply("INSERT 0 1") - utils.Config.Db.Seed.Enabled = false - // Run test - err := ResetAll(context.Background(), "", conn.MockClient(t), fsys) - // No error should be raised since we're skipping the seed - assert.NoError(t, err) - }) - - t.Run("throws error on drop schema failure", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(migration.DropObjects). - ReplyError(pgerrcode.InsufficientPrivilege, "permission denied for relation supabase_migrations") - // Run test - err := ResetAll(context.Background(), "", conn.MockClient(t), fsys) - // Check error - assert.ErrorContains(t, err, "ERROR: permission denied for relation supabase_migrations (SQLSTATE 42501)") - }) -} diff --git a/apps/cli-go/internal/migration/list/list.go b/apps/cli-go/internal/migration/list/list.go deleted file mode 100644 index ad4a062c06..0000000000 --- a/apps/cli-go/internal/migration/list/list.go +++ /dev/null @@ -1,87 +0,0 @@ -package list - -import ( - "context" - "fmt" - "math" - "strconv" - - "github.com/jackc/pgconn" - "github.com/jackc/pgx/v4" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/migration" -) - -func Run(ctx context.Context, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { - remoteVersions, err := loadRemoteVersions(ctx, config, options...) - if err != nil { - return err - } - localVersions, err := LoadLocalVersions(fsys) - if err != nil { - return err - } - table := makeTable(remoteVersions, localVersions) - return utils.RenderTable(table) -} - -func loadRemoteVersions(ctx context.Context, config pgconn.Config, options ...func(*pgx.ConnConfig)) ([]string, error) { - conn, err := utils.ConnectByConfig(ctx, config, options...) - if err != nil { - return nil, err - } - defer conn.Close(context.Background()) - return migration.ListRemoteMigrations(ctx, conn) -} - -func makeTable(remoteMigrations, localMigrations []string) string { - var err error - table := "|Local|Remote|Time (UTC)|\n|-|-|-|\n" - for i, j := 0, 0; i < len(remoteMigrations) || j < len(localMigrations); { - remoteTimestamp := math.MaxInt - if i < len(remoteMigrations) { - if remoteTimestamp, err = strconv.Atoi(remoteMigrations[i]); err != nil { - i++ - continue - } - } - localTimestamp := math.MaxInt - if j < len(localMigrations) { - if localTimestamp, err = strconv.Atoi(localMigrations[j]); err != nil { - j++ - continue - } - } - // Top to bottom chronological order - if localTimestamp < remoteTimestamp { - table += fmt.Sprintf("|`%s`|` `|`%s`|\n", localMigrations[j], utils.FormatTimestampVersion(localMigrations[j])) - j++ - } else if remoteTimestamp < localTimestamp { - table += fmt.Sprintf("|` `|`%s`|`%s`|\n", remoteMigrations[i], utils.FormatTimestampVersion(remoteMigrations[i])) - i++ - } else { - table += fmt.Sprintf("|`%s`|`%s`|`%s`|\n", localMigrations[j], remoteMigrations[i], utils.FormatTimestampVersion(remoteMigrations[i])) - i++ - j++ - } - } - return table -} - -func LoadLocalVersions(fsys afero.Fs) ([]string, error) { - var versions []string - filter := func(v string) bool { - versions = append(versions, v) - return true - } - _, err := migration.ListLocalMigrations(utils.MigrationsDir, afero.NewIOFS(fsys), filter) - return versions, err -} - -func LoadPartialMigrations(version string, fsys afero.Fs) ([]string, error) { - filter := func(v string) bool { - return version == "" || v <= version - } - return migration.ListLocalMigrations(utils.MigrationsDir, afero.NewIOFS(fsys), filter) -} diff --git a/apps/cli-go/internal/migration/list/list_test.go b/apps/cli-go/internal/migration/list/list_test.go deleted file mode 100644 index 9744174df9..0000000000 --- a/apps/cli-go/internal/migration/list/list_test.go +++ /dev/null @@ -1,193 +0,0 @@ -package list - -import ( - "context" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/jackc/pgconn" - "github.com/jackc/pgerrcode" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/testing/fstest" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/migration" - "github.com/supabase/cli/pkg/pgtest" -) - -var dbConfig = pgconn.Config{ - Host: "127.0.0.1", - Port: 5432, - User: "admin", - Password: "password", - Database: "postgres", -} - -func TestMigrationList(t *testing.T) { - t.Run("lists remote migrations", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(migration.LIST_MIGRATION_VERSION). - Reply("SELECT 0") - // Run test - err := Run(context.Background(), dbConfig, fsys, conn.Intercept) - // Check error - assert.NoError(t, err) - }) - - t.Run("throws error on remote failure", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Run test - err := Run(context.Background(), pgconn.Config{}, fsys) - // Check error - assert.ErrorContains(t, err, "invalid port (outside range)") - }) - - t.Run("throws error on open failure", func(t *testing.T) { - // Setup in-memory fs - fsys := &fstest.OpenErrorFs{DenyPath: utils.MigrationsDir} - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(migration.LIST_MIGRATION_VERSION). - Reply("SELECT 0") - // Run test - err := Run(context.Background(), dbConfig, fsys, conn.Intercept) - // Check error - assert.ErrorIs(t, err, os.ErrPermission) - }) -} - -func TestRemoteMigrations(t *testing.T) { - t.Run("loads migration versions", func(t *testing.T) { - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(migration.LIST_MIGRATION_VERSION). - Reply("SELECT 1", []any{"20220727064247"}) - // Run test - versions, err := loadRemoteVersions(context.Background(), dbConfig, conn.Intercept) - // Check error - assert.NoError(t, err) - assert.ElementsMatch(t, []string{"20220727064247"}, versions) - }) - - t.Run("throws error on connect failure", func(t *testing.T) { - // Run test - _, err := loadRemoteVersions(context.Background(), pgconn.Config{}) - // Check error - assert.ErrorContains(t, err, "invalid port (outside range)") - }) - - t.Run("loads empty migrations on missing table", func(t *testing.T) { - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(migration.LIST_MIGRATION_VERSION). - ReplyError(pgerrcode.UndefinedTable, "relation \"supabase_migrations.schema_migrations\" does not exist") - // Run test - versions, err := loadRemoteVersions(context.Background(), dbConfig, conn.Intercept) - // Check error - assert.NoError(t, err) - assert.Empty(t, versions) - }) - - t.Run("throws error on invalid row", func(t *testing.T) { - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(migration.LIST_MIGRATION_VERSION). - Reply("SELECT 1", []any{}) - // Run test - _, err := loadRemoteVersions(context.Background(), dbConfig, conn.Intercept) - // Check error - assert.ErrorContains(t, err, "number of field descriptions must equal number of destinations, got 0 and 1") - }) -} - -func TestLocalMigrations(t *testing.T) { - t.Run("loads migration versions", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - path := filepath.Join(utils.MigrationsDir, "20220727064246_test.sql") - require.NoError(t, afero.WriteFile(fsys, path, []byte{}, 0644)) - path = filepath.Join(utils.MigrationsDir, "20220727064248_test.sql") - require.NoError(t, afero.WriteFile(fsys, path, []byte{}, 0644)) - // Run test - versions, err := LoadLocalVersions(fsys) - // Check error - assert.NoError(t, err) - assert.ElementsMatch(t, []string{"20220727064246", "20220727064248"}, versions) - }) - - t.Run("ignores outdated and invalid files", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - path := filepath.Join(utils.MigrationsDir, "20211208000000_init.sql") - require.NoError(t, afero.WriteFile(fsys, path, []byte{}, 0644)) - path = filepath.Join(utils.MigrationsDir, "20211208000001_invalid.ts") - require.NoError(t, afero.WriteFile(fsys, path, []byte{}, 0644)) - // Run test - versions, err := LoadLocalVersions(fsys) - // Check error - assert.NoError(t, err) - assert.Empty(t, versions) - }) - - t.Run("throws error on open failure", func(t *testing.T) { - // Setup in-memory fs - fsys := &fstest.OpenErrorFs{DenyPath: utils.MigrationsDir} - // Run test - _, err := LoadLocalVersions(fsys) - // Check error - assert.ErrorIs(t, err, os.ErrPermission) - }) -} - -func TestMakeTable(t *testing.T) { - t.Run("tabulate version", func(t *testing.T) { - // Run test - table := makeTable([]string{"0", "2"}, []string{"0", "1"}) - // Check error - lines := strings.Split(strings.TrimSpace(table), "\n") - assert.ElementsMatch(t, []string{ - "|Local|Remote|Time (UTC)|", - "|-|-|-|", - "|`0`|`0`|`0`|", - "|`1`|` `|`1`|", - "|` `|`2`|`2`|", - }, lines) - }) - - t.Run("tabulate timestamp", func(t *testing.T) { - // Run test - table := makeTable([]string{"20220727064246", "20220727064248"}, []string{"20220727064246", "20220727064247"}) - // Check error - lines := strings.Split(strings.TrimSpace(table), "\n") - assert.ElementsMatch(t, []string{ - "|Local|Remote|Time (UTC)|", - "|-|-|-|", - "|`20220727064246`|`20220727064246`|`2022-07-27 06:42:46`|", - "|`20220727064247`|` `|`2022-07-27 06:42:47`|", - "|` `|`20220727064248`|`2022-07-27 06:42:48`|", - }, lines) - }) - - t.Run("ignores string values", func(t *testing.T) { - // Run test - table := makeTable([]string{"a", "c"}, []string{"a", "b"}) - // Check error - lines := strings.Split(strings.TrimSpace(table), "\n") - assert.ElementsMatch(t, []string{ - "|Local|Remote|Time (UTC)|", - "|-|-|-|", - }, lines) - }) -} diff --git a/apps/cli-go/internal/migration/new/new.go b/apps/cli-go/internal/migration/new/new.go deleted file mode 100644 index be232b5913..0000000000 --- a/apps/cli-go/internal/migration/new/new.go +++ /dev/null @@ -1,46 +0,0 @@ -package new - -import ( - "fmt" - "io" - "os" - "path/filepath" - - "github.com/go-errors/errors" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/utils" -) - -func Run(migrationName string, stdin afero.File, fsys afero.Fs) error { - path := GetMigrationPath(utils.GetCurrentTimestamp(), migrationName) - if err := utils.MkdirIfNotExistFS(fsys, filepath.Dir(path)); err != nil { - return err - } - f, err := fsys.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) - if err != nil { - return errors.Errorf("failed to open migration file: %w", err) - } - defer func() { - fmt.Println("Created new migration at " + utils.Bold(path)) - // File descriptor will always be closed when process quits - _ = f.Close() - }() - return CopyStdinIfExists(stdin, f) -} - -func GetMigrationPath(timestamp, name string) string { - fullName := fmt.Sprintf("%s_%s.sql", timestamp, name) - return filepath.Join(utils.MigrationsDir, fullName) -} - -func CopyStdinIfExists(stdin afero.File, dst io.Writer) error { - if fi, err := stdin.Stat(); err != nil { - return errors.Errorf("failed to initialise stdin: %w", err) - } else if (fi.Mode() & os.ModeCharDevice) == 0 { - // Ref: https://stackoverflow.com/a/26567513 - if _, err := io.Copy(dst, stdin); err != nil { - return errors.Errorf("failed to copy from stdin: %w", err) - } - } - return nil -} diff --git a/apps/cli-go/internal/migration/new/new_test.go b/apps/cli-go/internal/migration/new/new_test.go deleted file mode 100644 index 39e2fe1156..0000000000 --- a/apps/cli-go/internal/migration/new/new_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package new - -import ( - "os" - "path/filepath" - "testing" - - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/utils" -) - -func TestNewCommand(t *testing.T) { - t.Run("creates new migration file", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup empty stdin - stdin, err := fsys.Create("/dev/stdin") - require.NoError(t, err) - // Run test - assert.NoError(t, Run("test_migrate", stdin, fsys)) - // Validate output - files, err := afero.ReadDir(fsys, utils.MigrationsDir) - assert.NoError(t, err) - assert.Equal(t, 1, len(files)) - assert.Regexp(t, `([0-9]{14})_test_migrate\.sql`, files[0].Name()) - }) - - t.Run("streams content from pipe", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup stdin - r, w, err := os.Pipe() - require.NoError(t, err) - script := "create table pet;\ndrop table pet;\n" - _, err = w.WriteString(script) - require.NoError(t, err) - require.NoError(t, w.Close()) - // Run test - assert.NoError(t, Run("test_migrate", r, fsys)) - // Validate output - files, err := afero.ReadDir(fsys, utils.MigrationsDir) - assert.NoError(t, err) - assert.Equal(t, 1, len(files)) - path := filepath.Join(utils.MigrationsDir, files[0].Name()) - contents, err := afero.ReadFile(fsys, path) - assert.NoError(t, err) - assert.Equal(t, []byte(script), contents) - }) - - t.Run("throws error on failure to create directory", func(t *testing.T) { - // Setup read-only fs - fsys := afero.NewMemMapFs() - // Setup empty stdin - stdin, err := fsys.Create("/dev/stdin") - require.NoError(t, err) - // Run test - assert.Error(t, Run("test_migrate", stdin, afero.NewReadOnlyFs(fsys))) - }) - - t.Run("throws error on closed pipe", func(t *testing.T) { - // Setup read-only fs - fsys := afero.NewMemMapFs() - // Setup empty stdin - r, _, err := os.Pipe() - require.NoError(t, err) - require.NoError(t, r.Close()) - // Run test - assert.Error(t, Run("test_migrate", r, fsys)) - }) -} diff --git a/apps/cli-go/internal/migration/repair/repair.go b/apps/cli-go/internal/migration/repair/repair.go deleted file mode 100644 index a7dfee274e..0000000000 --- a/apps/cli-go/internal/migration/repair/repair.go +++ /dev/null @@ -1,108 +0,0 @@ -package repair - -import ( - "context" - "fmt" - "os" - "path/filepath" - "strconv" - - "github.com/go-errors/errors" - "github.com/jackc/pgconn" - "github.com/jackc/pgx/v4" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/migration/list" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/migration" -) - -const ( - Applied = "applied" - Reverted = "reverted" -) - -var ErrInvalidVersion = errors.New("invalid version number") - -func Run(ctx context.Context, config pgconn.Config, version []string, status string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { - for _, v := range version { - if _, err := strconv.Atoi(v); err != nil { - return errors.Errorf("failed to parse %s: %w", v, ErrInvalidVersion) - } - } - repairAll := len(version) == 0 - if repairAll { - msg := "Do you want to repair the entire migration history table to match local migration files?" - if shouldRepair, err := utils.NewConsole().PromptYesNo(ctx, msg, false); err != nil { - return err - } else if !shouldRepair { - return errors.New(context.Canceled) - } - local, err := list.LoadLocalVersions(fsys) - if err != nil { - return err - } - version = append(version, local...) - } - conn, err := utils.ConnectByConfig(ctx, config, options...) - if err != nil { - return err - } - defer conn.Close(context.Background()) - // Update migration history - if err = UpdateMigrationTable(ctx, conn, version, status, repairAll, fsys); err == nil { - utils.CmdSuggestion = fmt.Sprintf("Run %s to show the updated migration history.", utils.Aqua("supabase migration list")) - } - return err -} - -func UpdateMigrationTable(ctx context.Context, conn *pgx.Conn, version []string, status string, repairAll bool, fsys afero.Fs) error { - if err := migration.CreateMigrationTable(ctx, conn); err != nil { - return err - } - // Data statements don't mutate schemas, safe to use statement cache - batch := &pgx.Batch{} - if repairAll { - batch.Queue(migration.TRUNCATE_VERSION_TABLE) - } - switch status { - case Applied: - for _, v := range version { - f, err := NewMigrationFromVersion(v, fsys) - if err != nil { - return err - } - batch.Queue(migration.UPSERT_MIGRATION_VERSION, f.Version, f.Name, f.Statements) - } - case Reverted: - if !repairAll { - batch.Queue(migration.DELETE_MIGRATION_VERSION, version) - } - } - if err := conn.SendBatch(ctx, batch).Close(); err != nil { - return errors.Errorf("failed to update migration table: %w", err) - } - if !repairAll { - fmt.Fprintf(os.Stderr, "Repaired migration history: %v => %s\n", version, status) - } - return nil -} - -func GetMigrationFile(version string, fsys afero.Fs) (string, error) { - path := filepath.Join(utils.MigrationsDir, version+"_*.sql") - matches, err := afero.Glob(fsys, path) - if err != nil { - return "", errors.Errorf("failed to glob migration files: %w", err) - } - if len(matches) == 0 { - return "", errors.Errorf("glob %s: %w", path, os.ErrNotExist) - } - return matches[0], nil -} - -func NewMigrationFromVersion(version string, fsys afero.Fs) (*migration.MigrationFile, error) { - name, err := GetMigrationFile(version, fsys) - if err != nil { - return nil, err - } - return migration.NewMigrationFromFile(name, afero.NewIOFS(fsys)) -} diff --git a/apps/cli-go/internal/migration/repair/repair_test.go b/apps/cli-go/internal/migration/repair/repair_test.go deleted file mode 100644 index 4c0bdb7cf3..0000000000 --- a/apps/cli-go/internal/migration/repair/repair_test.go +++ /dev/null @@ -1,160 +0,0 @@ -package repair - -import ( - "context" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/jackc/pgconn" - "github.com/jackc/pgerrcode" - "github.com/jackc/pgx/v4" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/testing/fstest" - "github.com/supabase/cli/internal/testing/helper" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/migration" - "github.com/supabase/cli/pkg/pgtest" -) - -var dbConfig = pgconn.Config{ - Host: "db.supabase.com", - Port: 5432, - User: "admin", - Password: "password", - Database: "postgres", -} - -func TestRepairCommand(t *testing.T) { - t.Run("applies new version", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - path := filepath.Join(utils.MigrationsDir, "0_test.sql") - require.NoError(t, afero.WriteFile(fsys, path, []byte("select 1"), 0644)) - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - helper.MockMigrationHistory(conn). - Query(migration.UPSERT_MIGRATION_VERSION, "0", "test", []string{"select 1"}). - Reply("INSERT 0 1") - // Run test - err := Run(context.Background(), dbConfig, []string{"0"}, Applied, fsys, conn.Intercept) - // Check error - assert.NoError(t, err) - }) - - t.Run("reverts old version", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - helper.MockMigrationHistory(conn). - Query(migration.DELETE_MIGRATION_VERSION, []string{"0"}). - Reply("DELETE 1") - // Run test - err := Run(context.Background(), dbConfig, []string{"0"}, Reverted, fsys, conn.Intercept) - // Check error - assert.NoError(t, err) - }) - - t.Run("throws error on invalid version", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Run test - err := Run(context.Background(), pgconn.Config{}, []string{"invalid"}, Applied, fsys) - // Check error - assert.ErrorIs(t, err, ErrInvalidVersion) - }) - - t.Run("throws error on connect failure", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Run test - err := Run(context.Background(), pgconn.Config{}, []string{"0"}, Applied, fsys) - // Check error - assert.ErrorContains(t, err, "invalid port (outside range)") - }) - - t.Run("throws error on insert failure", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - path := filepath.Join(utils.MigrationsDir, "0_test.sql") - require.NoError(t, afero.WriteFile(fsys, path, []byte(""), 0644)) - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - helper.MockMigrationHistory(conn). - Query(migration.UPSERT_MIGRATION_VERSION, "0", "test", nil). - ReplyError(pgerrcode.DuplicateObject, `relation "supabase_migrations.schema_migrations" does not exist`) - // Run test - err := Run(context.Background(), dbConfig, []string{"0"}, Applied, fsys, conn.Intercept) - // Check error - assert.ErrorContains(t, err, `ERROR: relation "supabase_migrations.schema_migrations" does not exist (SQLSTATE 42710)`) - }) -} - -func TestRepairAll(t *testing.T) { - t.Run("repairs whole history", func(t *testing.T) { - t.Cleanup(fstest.MockStdin(t, "y")) - // Setup in-memory fs - fsys := afero.NewMemMapFs() - path := filepath.Join(utils.MigrationsDir, "0_test.sql") - require.NoError(t, afero.WriteFile(fsys, path, []byte("select 1"), 0644)) - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - helper.MockMigrationHistory(conn). - Query(strings.Join([]string{ - migration.TRUNCATE_VERSION_TABLE, - strings.ReplaceAll(migration.UPSERT_MIGRATION_VERSION, "$1, $2, $3", " '0' , 'test' , '{select 1}' "), - }, ";")). - Reply("TRUNCATE TABLE"). - Reply("INSERT 0 1") - // Run test - err := Run(context.Background(), dbConfig, nil, Applied, fsys, conn.Intercept, func(cc *pgx.ConnConfig) { - cc.PreferSimpleProtocol = true - }) - // Check error - assert.NoError(t, err) - }) - - t.Run("reverts whole history", func(t *testing.T) { - t.Cleanup(fstest.MockStdin(t, "y")) - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - helper.MockMigrationHistory(conn). - Query(migration.TRUNCATE_VERSION_TABLE). - Reply("TRUNCATE TABLE") - // Run test - err := Run(context.Background(), dbConfig, nil, Reverted, fsys, conn.Intercept) - // Check error - assert.NoError(t, err) - }) - - t.Run("throws error on cancel", func(t *testing.T) { - t.Cleanup(fstest.MockStdin(t, "n")) - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Run test - err := Run(context.Background(), dbConfig, nil, Applied, fsys) - // Check error - assert.ErrorIs(t, err, context.Canceled) - }) - - t.Run("throws error on permission denied", func(t *testing.T) { - t.Cleanup(fstest.MockStdin(t, "y")) - // Setup in-memory fs - fsys := &fstest.OpenErrorFs{DenyPath: utils.MigrationsDir} - // Run test - err := Run(context.Background(), dbConfig, nil, Applied, fsys) - // Check error - assert.ErrorIs(t, err, os.ErrPermission) - }) -} diff --git a/apps/cli-go/internal/seed/buckets/buckets.go b/apps/cli-go/internal/seed/buckets/buckets.go deleted file mode 100644 index e077f5e716..0000000000 --- a/apps/cli-go/internal/seed/buckets/buckets.go +++ /dev/null @@ -1,84 +0,0 @@ -package buckets - -import ( - "context" - "fmt" - "os" - "strings" - - "github.com/spf13/afero" - "github.com/supabase/cli/internal/storage/client" - "github.com/supabase/cli/internal/utils" -) - -func Run(ctx context.Context, projectRef string, interactive bool, fsys afero.Fs) error { - hasVectorBuckets := len(utils.Config.Storage.VectorBuckets.Buckets) > 0 - if len(projectRef) == 0 && - len(utils.Config.Storage.Buckets) == 0 && - !hasVectorBuckets { - return nil - } - api, err := client.NewStorageAPI(ctx, projectRef) - if err != nil { - return err - } - console := utils.NewConsole() - if !interactive { - console.IsTTY = false - } - filter := func(bucketId string) bool { - label := fmt.Sprintf("Bucket %s already exists. Do you want to overwrite its properties?", utils.Bold(bucketId)) - shouldOverwrite, err := console.PromptYesNo(ctx, label, true) - if err != nil { - fmt.Fprintln(utils.GetDebugLogger(), err) - } - return shouldOverwrite - } - if err := api.UpsertBuckets(ctx, utils.Config.Storage.Buckets, filter); err != nil { - return err - } - prune := func(name string) bool { - label := fmt.Sprintf("Bucket %s not found in %s. Do you want to prune it?", utils.Bold(name), utils.Bold(utils.ConfigPath)) - shouldPrune, err := console.PromptYesNo(ctx, label, false) - if err != nil { - fmt.Fprintln(utils.GetDebugLogger(), err) - } - return shouldPrune - } - if utils.Config.Storage.AnalyticsBuckets.Enabled && len(projectRef) > 0 { - fmt.Fprintln(os.Stderr, "Updating analytics buckets...") - if err := api.UpsertAnalyticsBuckets(ctx, utils.Config.Storage.AnalyticsBuckets.Buckets, prune); err != nil { - return err - } - } - if utils.Config.Storage.VectorBuckets.Enabled && hasVectorBuckets { - fmt.Fprintln(os.Stderr, "Updating vector buckets...") - if err := api.UpsertVectorBuckets(ctx, utils.Config.Storage.VectorBuckets.Buckets, prune); err != nil { - if isVectorBucketsFeatureNotEnabled(err) { - fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "Vector buckets are not available in this project's region yet. Skipping vector bucket seeding.") - return api.UpsertObjects(ctx, utils.Config.Storage.Buckets, utils.NewRootFS(fsys)) - } - if isLocalVectorBucketsUnavailable(err) { - fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "Vector buckets are not available in the local storage service. If this project is linked, run `supabase link` to update service versions, then restart the local stack. Skipping vector bucket seeding.") - return api.UpsertObjects(ctx, utils.Config.Storage.Buckets, utils.NewRootFS(fsys)) - } - return err - } - } - return api.UpsertObjects(ctx, utils.Config.Storage.Buckets, utils.NewRootFS(fsys)) -} - -func isVectorBucketsFeatureNotEnabled(err error) bool { - return err != nil && strings.Contains(err.Error(), "FeatureNotEnabled") -} - -func isLocalVectorBucketsUnavailable(err error) bool { - if err == nil { - return false - } - message := err.Error() - return strings.Contains(message, "Vector service not configured") || - (strings.Contains(message, "Error status 404:") && - strings.Contains(message, "Route POST:") && - strings.Contains(message, "ListVectorBuckets")) -} diff --git a/apps/cli-go/internal/seed/buckets/buckets_test.go b/apps/cli-go/internal/seed/buckets/buckets_test.go deleted file mode 100644 index 9527d3f554..0000000000 --- a/apps/cli-go/internal/seed/buckets/buckets_test.go +++ /dev/null @@ -1,297 +0,0 @@ -package buckets - -import ( - "context" - "io" - "net/http" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/BurntSushi/toml" - "github.com/h2non/gock" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/testing/apitest" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/storage" -) - -func TestSeedBuckets(t *testing.T) { - t.Run("seeds buckets", func(t *testing.T) { - t.Cleanup(func() { clear(utils.Config.Storage.Buckets) }) - config := ` -[test] -public = true -[private] -public = false` - require.NoError(t, toml.Unmarshal([]byte(config), &utils.Config.Storage.Buckets)) - // Setup in-memory fs - fsys := afero.NewMemMapFs() - bucketPath := filepath.Join(utils.SupabaseDirPath, "images") - require.NoError(t, fsys.Mkdir(bucketPath, 0755)) - // Setup mock api - defer gock.OffAll() - gock.New(utils.Config.Api.ExternalUrl). - Get("/storage/v1/bucket"). - Reply(http.StatusOK). - JSON([]storage.BucketResponse{{ - Name: "test", - Id: "test", - }}) - gock.New(utils.Config.Api.ExternalUrl). - Put("/storage/v1/bucket/test"). - Reply(http.StatusOK). - JSON(storage.UpdateBucketResponse{}) - gock.New(utils.Config.Api.ExternalUrl). - Post("/storage/v1/bucket"). - Reply(http.StatusOK). - JSON(storage.CreateBucketResponse{Name: "private"}) - // Run test - err := Run(context.Background(), "", false, fsys) - // Check error - assert.NoError(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("ignores unconfigured buckets", func(t *testing.T) { - t.Cleanup(func() { - utils.Config.Storage.TargetMigration = "" - gock.OffAll() - }) - utils.Config.Storage.TargetMigration = "custom-metadata" - gock.New(utils.Config.Api.ExternalUrl). - Get("/storage/v1/bucket"). - Reply(http.StatusBadRequest). - JSON(map[string]string{ - "statusCode": "403", - "error": "Unauthorized", - "message": "new row violates row-level security policy", - }) - // Run test - err := Run(context.Background(), "", false, afero.NewMemMapFs()) - // Check error - assert.NoError(t, err) - assert.Len(t, gock.Pending(), 1) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("does not call vector API when no vector buckets are configured", func(t *testing.T) { - t.Cleanup(func() { - clear(utils.Config.Storage.Buckets) - utils.Config.Storage.VectorBuckets.Enabled = false - clear(utils.Config.Storage.VectorBuckets.Buckets) - gock.OffAll() - }) - config := ` -[images] -public = true` - require.NoError(t, toml.Unmarshal([]byte(config), &utils.Config.Storage.Buckets)) - utils.Config.Storage.VectorBuckets.Enabled = true - utils.Config.Storage.VectorBuckets.Buckets = map[string]struct{}{} - - gock.New(utils.Config.Api.ExternalUrl). - Get("/storage/v1/bucket"). - Reply(http.StatusOK). - JSON([]storage.BucketResponse{}) - gock.New(utils.Config.Api.ExternalUrl). - Post("/storage/v1/bucket"). - Reply(http.StatusOK). - JSON(storage.CreateBucketResponse{Name: "images"}) - - err := Run(context.Background(), "", false, afero.NewMemMapFs()) - - assert.NoError(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("does not call storage API when no buckets are configured", func(t *testing.T) { - t.Cleanup(func() { - utils.Config.Storage.VectorBuckets.Enabled = false - clear(utils.Config.Storage.VectorBuckets.Buckets) - gock.OffAll() - }) - utils.Config.Storage.VectorBuckets.Enabled = true - utils.Config.Storage.VectorBuckets.Buckets = map[string]struct{}{} - - err := Run(context.Background(), "", false, afero.NewMemMapFs()) - - assert.NoError(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("seeds vector buckets locally", func(t *testing.T) { - t.Cleanup(func() { - utils.Config.Storage.VectorBuckets.Enabled = false - clear(utils.Config.Storage.VectorBuckets.Buckets) - gock.OffAll() - }) - utils.Config.Storage.VectorBuckets.Enabled = true - utils.Config.Storage.VectorBuckets.Buckets = map[string]struct{}{ - "documents-openai": {}, - "existing-vec": {}, - } - // Setup mock api: regular buckets list is empty, vector list has one - // configured bucket plus one stale bucket that should be left alone - // because non-interactive prune defaults to false. - gock.New(utils.Config.Api.ExternalUrl). - Get("/storage/v1/bucket"). - Reply(http.StatusOK). - JSON([]storage.BucketResponse{}) - gock.New(utils.Config.Api.ExternalUrl). - Post("/storage/v1/vector/ListVectorBuckets"). - Reply(http.StatusOK). - JSON(storage.ListVectorBucketsResponse{ - VectorBuckets: []storage.VectorBucket{ - {VectorBucketName: "existing-vec"}, - {VectorBucketName: "stale-vec"}, - }, - }) - gock.New(utils.Config.Api.ExternalUrl). - Post("/storage/v1/vector/CreateVectorBucket"). - Reply(http.StatusOK). - JSON(map[string]string{}) - gock.New(utils.Config.Api.ExternalUrl). - Post("/storage/v1/vector/DeleteVectorBucket"). - Reply(http.StatusOK). - JSON(map[string]string{}) - // Run test - err := Run(context.Background(), "", false, afero.NewMemMapFs()) - // Check error - assert.NoError(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) - // The DeleteVectorBucket mock should remain pending because non-interactive - // prune returns the default (false) and skips the delete. - pending := gock.Pending() - require.Len(t, pending, 1) - assert.Contains(t, pending[0].Request().URLStruct.Path, "DeleteVectorBucket") - }) - - t.Run("warns and continues when vector buckets are unavailable in the region", func(t *testing.T) { - t.Cleanup(func() { - utils.Config.Storage.VectorBuckets.Enabled = false - clear(utils.Config.Storage.VectorBuckets.Buckets) - gock.OffAll() - }) - utils.Config.Storage.VectorBuckets.Enabled = true - utils.Config.Storage.VectorBuckets.Buckets = map[string]struct{}{ - "documents-openai": {}, - } - - gock.New(utils.Config.Api.ExternalUrl). - Get("/storage/v1/bucket"). - Reply(http.StatusOK). - JSON([]storage.BucketResponse{}) - gock.New(utils.Config.Api.ExternalUrl). - Post("/storage/v1/vector/ListVectorBuckets"). - Reply(http.StatusBadRequest). - JSON(map[string]string{ - "code": "FeatureNotEnabled", - "message": "Feature is not enabled", - }) - - stderr := captureStderr(t, func() { - err := Run(context.Background(), "", false, afero.NewMemMapFs()) - assert.NoError(t, err) - }) - - assert.Contains(t, stderr, "WARNING:") - assert.Contains(t, stderr, "Vector buckets are not available in this project's region yet") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("warns and continues when local vector storage is not configured", func(t *testing.T) { - t.Cleanup(func() { - utils.Config.Storage.VectorBuckets.Enabled = false - clear(utils.Config.Storage.VectorBuckets.Buckets) - gock.OffAll() - }) - utils.Config.Storage.VectorBuckets.Enabled = true - utils.Config.Storage.VectorBuckets.Buckets = map[string]struct{}{ - "documents-openai": {}, - } - - gock.New(utils.Config.Api.ExternalUrl). - Get("/storage/v1/bucket"). - Reply(http.StatusOK). - JSON([]storage.BucketResponse{}) - gock.New(utils.Config.Api.ExternalUrl). - Post("/storage/v1/vector/ListVectorBuckets"). - Reply(http.StatusConflict). - JSON(map[string]any{ - "statusCode": http.StatusConflict, - "code": "InvalidRequest", - "error": "InvalidRequest", - "message": "The feature Vector service not configured is not enabled for this resource", - }) - - stderr := captureStderr(t, func() { - err := Run(context.Background(), "", false, afero.NewMemMapFs()) - assert.NoError(t, err) - }) - - assert.Contains(t, stderr, "WARNING:") - assert.Contains(t, stderr, "Vector buckets are not available in the local storage service") - assert.Contains(t, stderr, "supabase link") - assert.Contains(t, stderr, "restart the local stack") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("warns and continues when local vector routes are not registered", func(t *testing.T) { - t.Cleanup(func() { - utils.Config.Storage.VectorBuckets.Enabled = false - clear(utils.Config.Storage.VectorBuckets.Buckets) - gock.OffAll() - }) - utils.Config.Storage.VectorBuckets.Enabled = true - utils.Config.Storage.VectorBuckets.Buckets = map[string]struct{}{ - "documents-openai": {}, - } - - gock.New(utils.Config.Api.ExternalUrl). - Get("/storage/v1/bucket"). - Reply(http.StatusOK). - JSON([]storage.BucketResponse{}) - gock.New(utils.Config.Api.ExternalUrl). - Post("/storage/v1/vector/ListVectorBuckets"). - Reply(http.StatusNotFound). - JSON(map[string]any{ - "statusCode": http.StatusNotFound, - "error": "Not Found", - "message": "Route POST:/vector/ListVectorBuckets not found", - }) - - stderr := captureStderr(t, func() { - err := Run(context.Background(), "", false, afero.NewMemMapFs()) - assert.NoError(t, err) - }) - - assert.Contains(t, stderr, "WARNING:") - assert.Contains(t, stderr, "Vector buckets are not available in the local storage service") - assert.Contains(t, stderr, "supabase link") - assert.Contains(t, stderr, "restart the local stack") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} - -func captureStderr(t *testing.T, run func()) string { - t.Helper() - reader, writer, err := os.Pipe() - require.NoError(t, err) - original := os.Stderr - os.Stderr = writer - t.Cleanup(func() { - os.Stderr = original - reader.Close() - }) - - run() - - require.NoError(t, writer.Close()) - os.Stderr = original - out, err := io.ReadAll(reader) - require.NoError(t, err) - return strings.TrimSpace(string(out)) -} diff --git a/apps/cli-go/internal/status/status.go b/apps/cli-go/internal/status/status.go deleted file mode 100644 index 6596319813..0000000000 --- a/apps/cli-go/internal/status/status.go +++ /dev/null @@ -1,392 +0,0 @@ -package status - -import ( - "context" - "crypto/tls" - "crypto/x509" - _ "embed" - "fmt" - "io" - "net/http" - "net/url" - "os" - "slices" - "sync" - "time" - - "github.com/Netflix/go-env" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/go-errors/errors" - "github.com/olekukonko/tablewriter" - "github.com/olekukonko/tablewriter/tw" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/internal/utils/flags" - "github.com/supabase/cli/pkg/fetcher" -) - -type CustomName struct { - ApiURL string `env:"api.url,default=API_URL"` - RestURL string `env:"api.rest_url,default=REST_URL"` - GraphqlURL string `env:"api.graphql_url,default=GRAPHQL_URL"` - StorageS3URL string `env:"api.storage_s3_url,default=STORAGE_S3_URL"` - McpURL string `env:"api.mcp_url,default=MCP_URL"` - FunctionsURL string `env:"api.functions_url,default=FUNCTIONS_URL"` - DbURL string `env:"db.url,default=DB_URL"` - StudioURL string `env:"studio.url,default=STUDIO_URL"` - InbucketURL string `env:"inbucket.url,default=INBUCKET_URL,deprecated"` - MailpitURL string `env:"mailpit.url,default=MAILPIT_URL"` - PublishableKey string `env:"auth.publishable_key,default=PUBLISHABLE_KEY"` - SecretKey string `env:"auth.secret_key,default=SECRET_KEY"` - JWTSecret string `env:"auth.jwt_secret,default=JWT_SECRET,deprecated"` - AnonKey string `env:"auth.anon_key,default=ANON_KEY,deprecated"` - ServiceRoleKey string `env:"auth.service_role_key,default=SERVICE_ROLE_KEY,deprecated"` - StorageS3AccessKeyId string `env:"storage.s3_access_key_id,default=S3_PROTOCOL_ACCESS_KEY_ID"` - StorageS3SecretAccessKey string `env:"storage.s3_secret_access_key,default=S3_PROTOCOL_ACCESS_KEY_SECRET"` - StorageS3Region string `env:"storage.s3_region,default=S3_PROTOCOL_REGION"` -} - -func (c *CustomName) toValues(exclude ...string) map[string]string { - values := map[string]string{ - c.DbURL: fmt.Sprintf("postgresql://%s@%s:%d/postgres", url.UserPassword("postgres", utils.Config.Db.Password), utils.Config.Hostname, utils.Config.Db.Port), - } - - kongEnabled := utils.Config.Api.Enabled && !slices.Contains(exclude, utils.KongId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Api.KongImage)) - postgrestEnabled := kongEnabled && !slices.Contains(exclude, utils.RestId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Api.Image)) - studioEnabled := utils.Config.Studio.Enabled && !slices.Contains(exclude, utils.StudioId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Studio.Image)) - authEnabled := utils.Config.Auth.Enabled && !slices.Contains(exclude, utils.GotrueId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Auth.Image)) - inbucketEnabled := utils.Config.Inbucket.Enabled && !slices.Contains(exclude, utils.InbucketId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Inbucket.Image)) - storageEnabled := utils.Config.Storage.Enabled && !slices.Contains(exclude, utils.StorageId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Storage.Image)) - functionsEnabled := utils.Config.EdgeRuntime.Enabled && !slices.Contains(exclude, utils.EdgeRuntimeId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.EdgeRuntime.Image)) - - if kongEnabled { - values[c.ApiURL] = utils.Config.Api.ExternalUrl - if postgrestEnabled { - values[c.RestURL] = utils.GetApiUrl("/rest/v1") - values[c.GraphqlURL] = utils.GetApiUrl("/graphql/v1") - } - if functionsEnabled { - values[c.FunctionsURL] = utils.GetApiUrl("/functions/v1") - } - if studioEnabled { - values[c.McpURL] = utils.GetApiUrl("/mcp") - } - } - if studioEnabled { - values[c.StudioURL] = fmt.Sprintf("http://%s:%d", utils.Config.Hostname, utils.Config.Studio.Port) - } - if authEnabled { - values[c.PublishableKey] = utils.Config.Auth.PublishableKey.Value - values[c.SecretKey] = utils.Config.Auth.SecretKey.Value - values[c.JWTSecret] = utils.Config.Auth.JwtSecret.Value - values[c.AnonKey] = utils.Config.Auth.AnonKey.Value - values[c.ServiceRoleKey] = utils.Config.Auth.ServiceRoleKey.Value - } - if inbucketEnabled { - values[c.MailpitURL] = fmt.Sprintf("http://%s:%d", utils.Config.Hostname, utils.Config.Inbucket.Port) - values[c.InbucketURL] = fmt.Sprintf("http://%s:%d", utils.Config.Hostname, utils.Config.Inbucket.Port) - } - if storageEnabled && utils.Config.Storage.S3Protocol != nil && utils.Config.Storage.S3Protocol.Enabled { - values[c.StorageS3URL] = utils.GetApiUrl("/storage/v1/s3") - values[c.StorageS3AccessKeyId] = utils.Config.Storage.S3Credentials.AccessKeyId - values[c.StorageS3SecretAccessKey] = utils.Config.Storage.S3Credentials.SecretAccessKey - values[c.StorageS3Region] = utils.Config.Storage.S3Credentials.Region - } - return values -} - -func Run(ctx context.Context, names CustomName, format string, fsys afero.Fs, ignoreHealthCheck bool, exclude ...string) error { - // Sanity checks. - if err := flags.LoadConfig(fsys); err != nil { - return err - } - if !ignoreHealthCheck { - if err := assertContainerHealthy(ctx, utils.DbId); err != nil { - return err - } - } - stopped, err := checkServiceHealth(ctx) - if err != nil { - return err - } - if len(stopped) > 0 { - fmt.Fprintln(os.Stderr, "Stopped services:", stopped) - } - excluded := append(stopped, exclude...) - if format == utils.OutputPretty { - fmt.Fprintf(os.Stderr, "%s local development setup is running.\n\n", utils.Aqua("supabase")) - PrettyPrint(os.Stdout, excluded...) - return nil - } - return printStatus(names, format, os.Stdout, excluded...) -} - -func checkServiceHealth(ctx context.Context) ([]string, error) { - resp, err := utils.Docker.ContainerList(ctx, container.ListOptions{ - Filters: utils.CliProjectFilter(utils.Config.ProjectId), - }) - if err != nil { - return nil, errors.Errorf("failed to list running containers: %w", err) - } - running := make(map[string]struct{}, len(resp)) - for _, c := range resp { - for _, n := range c.Names { - running[n] = struct{}{} - } - } - var stopped []string - for _, containerId := range utils.GetDockerIds() { - if _, ok := running["/"+containerId]; !ok { - stopped = append(stopped, containerId) - } - } - return stopped, nil -} - -func assertContainerHealthy(ctx context.Context, container string) error { - if resp, err := utils.Docker.ContainerInspect(ctx, container); err != nil { - return errors.Errorf("failed to inspect container health: %w", err) - } else if !resp.State.Running { - return errors.Errorf("%s container is not running: %s", container, resp.State.Status) - } else if resp.State.Health != nil && resp.State.Health.Status != types.Healthy { - return errors.Errorf("%s container is not ready: %s", container, resp.State.Health.Status) - } - return nil -} - -func IsServiceReady(ctx context.Context, container string) error { - if container == utils.RestId { - // PostgREST does not support native health checks - return checkHTTPHead(ctx, "/rest-admin/v1/ready") - } - if container == utils.EdgeRuntimeId { - // Native health check logs too much hyper::Error(IncompleteMessage) - return checkHTTPHead(ctx, "/functions/v1/_internal/health") - } - return assertContainerHealthy(ctx, container) -} - -// NewKongClient returns an HTTP client configured for the local Kong gateway. -// It deliberately omits http.Client.Timeout: this client is shared with -// streaming storage uploads, where a full-request deadline would truncate -// large transfers under load. Short-lived callers (e.g. health probes) should -// bound themselves with a per-call context deadline instead. -// -// To regenerate local certificate pair: -// -// openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 \ -// -nodes -keyout kong.local.key -out kong.local.crt -subj "/CN=localhost" \ -// -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" -func NewKongClient() *http.Client { - client := &http.Client{} - if t, ok := http.DefaultTransport.(*http.Transport); ok { - pool, err := x509.SystemCertPool() - if err != nil { - fmt.Fprintln(utils.GetDebugLogger(), err) - pool = x509.NewCertPool() - } - // No need to replace TLS config if we fail to append cert - if pool.AppendCertsFromPEM(utils.Config.Api.Tls.CertContent) { - rt := t.Clone() - rt.TLSClientConfig = &tls.Config{ - MinVersion: tls.VersionTLS12, - RootCAs: pool, - } - client.Transport = rt - } - } - return client -} - -var ( - healthClient *fetcher.Fetcher - healthOnce sync.Once -) - -// healthProbeTimeout caps a single readiness probe so a hung response cannot -// stall the surrounding retry loop in WaitForHealthyService. -const healthProbeTimeout = 10 * time.Second - -func checkHTTPHead(ctx context.Context, path string) error { - healthOnce.Do(func() { - healthClient = fetcher.NewServiceGateway( - utils.Config.Api.ExternalUrl, - utils.Config.Auth.SecretKey.Value, - fetcher.WithHTTPClient(NewKongClient()), - fetcher.WithUserAgent("SupabaseCLI/"+utils.Version), - ) - }) - ctx, cancel := context.WithTimeout(ctx, healthProbeTimeout) - defer cancel() - // HEAD method does not return response body - resp, err := healthClient.Send(ctx, http.MethodHead, path, nil) - if err != nil { - return err - } - defer resp.Body.Close() - return nil -} - -func printStatus(names CustomName, format string, w io.Writer, exclude ...string) (err error) { - values := names.toValues(exclude...) - return utils.EncodeOutput(format, w, values) -} - -func PrettyPrint(w io.Writer, exclude ...string) { - logger := utils.GetDebugLogger() - - names := CustomName{} - if err := env.Unmarshal(env.EnvSet{}, &names); err != nil { - fmt.Fprintln(logger, err) - } - values := names.toValues(exclude...) - - groups := []OutputGroup{ - { - Name: "🔧 Development Tools", - Items: []OutputItem{ - {Label: "Studio", Value: values[names.StudioURL], Type: Link}, - {Label: "Mailpit", Value: values[names.MailpitURL], Type: Link}, - {Label: "MCP", Value: values[names.McpURL], Type: Link}, - }, - }, - { - Name: "🌐 APIs", - Items: []OutputItem{ - {Label: "Project URL", Value: values[names.ApiURL], Type: Link}, - {Label: "REST", Value: values[names.RestURL], Type: Link}, - {Label: "GraphQL", Value: values[names.GraphqlURL], Type: Link}, - {Label: "Edge Functions", Value: values[names.FunctionsURL], Type: Link}, - }, - }, - { - Name: "⛁ Database", - Items: []OutputItem{ - {Label: "URL", Value: values[names.DbURL], Type: Link}, - }, - }, - { - Name: "🔑 Authentication Keys", - Items: []OutputItem{ - {Label: "Publishable", Value: values[names.PublishableKey], Type: Key}, - {Label: "Secret", Value: values[names.SecretKey], Type: Key}, - }, - }, - { - Name: "📦 Storage (S3)", - Items: []OutputItem{ - {Label: "URL", Value: values[names.StorageS3URL], Type: Link}, - {Label: "Access Key", Value: values[names.StorageS3AccessKeyId], Type: Key}, - {Label: "Secret Key", Value: values[names.StorageS3SecretAccessKey], Type: Key}, - {Label: "Region", Value: values[names.StorageS3Region], Type: Text}, - }, - }, - } - - for _, group := range groups { - if err := group.printTable(w); err != nil { - fmt.Fprintln(logger, err) - } else { - fmt.Fprintln(w) - } - } -} - -type OutputType string - -const ( - Text OutputType = "text" - Link OutputType = "link" - Key OutputType = "key" -) - -type OutputItem struct { - Label string - Value string - Type OutputType -} - -type OutputGroup struct { - Name string - Items []OutputItem -} - -func (g *OutputGroup) printTable(w io.Writer) error { - table := tablewriter.NewTable(w, - // Rounded corners - tablewriter.WithSymbols(tw.NewSymbols(tw.StyleRounded)), - - // Table content formatting - tablewriter.WithConfig(tablewriter.Config{ - Header: tw.CellConfig{ - Formatting: tw.CellFormatting{ - AutoFormat: tw.Off, - MergeMode: tw.MergeHorizontal, - }, - Alignment: tw.CellAlignment{ - Global: tw.AlignLeft, - }, - Filter: tw.CellFilter{ - Global: func(s []string) []string { - for i := range s { - s[i] = utils.Bold(s[i]) - } - return s - }, - }, - }, - Row: tw.CellConfig{ - Alignment: tw.CellAlignment{ - Global: tw.AlignLeft, - }, - ColMaxWidths: tw.CellWidth{ - PerColumn: map[int]int{0: 16}, - }, - Filter: tw.CellFilter{ - PerColumn: []func(string) string{ - func(s string) string { - return utils.Green(s) - }, - }, - }, - }, - Behavior: tw.Behavior{ - Compact: tw.Compact{ - Merge: tw.On, - }, - }, - }), - - // Set title as header (merged across all columns) - tablewriter.WithHeader([]string{g.Name, g.Name}), - ) - - // Add data rows with values colored based on type - shouldRender := false - for _, row := range g.Items { - if row.Value == "" { - continue - } - value := row.Value - switch row.Type { - case Link: - value = utils.Aqua(row.Value) - case Key: - value = utils.Yellow(row.Value) - } - if err := table.Append(row.Label, value); err != nil { - return errors.Errorf("failed to append row: %w", err) - } - shouldRender = true - } - - // Ensure at least one item in the group is non-empty - if shouldRender { - if err := table.Render(); err != nil { - return errors.Errorf("failed to render table: %w", err) - } - } - - return nil -} diff --git a/apps/cli-go/internal/status/status_test.go b/apps/cli-go/internal/status/status_test.go deleted file mode 100644 index 37b309b7b1..0000000000 --- a/apps/cli-go/internal/status/status_test.go +++ /dev/null @@ -1,214 +0,0 @@ -package status - -import ( - "bytes" - "context" - "errors" - "net/http" - "testing" - - "github.com/docker/docker/api/types/container" - "github.com/h2non/gock" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/testing/apitest" - "github.com/supabase/cli/internal/utils" -) - -func TestStatusCommand(t *testing.T) { - t.Run("shows service status", func(t *testing.T) { - var running []container.Summary - for _, name := range utils.GetDockerIds() { - running = append(running, container.Summary{ - Names: []string{name + "_test"}, - }) - } - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, utils.InitConfig(utils.InitParams{ProjectId: "test"}, fsys)) - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/supabase_db_test/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{ - Running: true, - }, - }}) - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/json"). - Reply(http.StatusOK). - JSON(running) - // Run test - assert.NoError(t, Run(context.Background(), CustomName{}, utils.OutputPretty, fsys, false)) - // Check error - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("skips database health check when ignored", func(t *testing.T) { - var running []container.Summary - for _, name := range utils.GetDockerIds() { - running = append(running, container.Summary{ - Names: []string{name + "_test"}, - }) - } - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, utils.InitConfig(utils.InitParams{ProjectId: "test"}, fsys)) - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/json"). - Reply(http.StatusOK). - JSON(running) - // Run test - assert.NoError(t, Run(context.Background(), CustomName{}, utils.OutputPretty, fsys, true)) - // Check error - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on malformed config", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, afero.WriteFile(fsys, utils.ConfigPath, []byte("malformed"), 0644)) - // Run test - err := Run(context.Background(), CustomName{}, utils.OutputPretty, fsys, false) - // Check error - assert.ErrorContains(t, err, "toml: expected = after a key, but the document ends there") - }) - - t.Run("throws error on missing docker", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/supabase_db_"). - ReplyError(errors.New("network error")) - // Run test - err := Run(context.Background(), CustomName{}, utils.OutputPretty, fsys, false) - // Check error - assert.ErrorContains(t, err, "network error") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} - -func TestServiceHealth(t *testing.T) { - t.Run("checks all services", func(t *testing.T) { - var running []container.Summary - for _, name := range utils.GetDockerIds() { - running = append(running, container.Summary{ - Names: []string{"/" + name}, - }) - } - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/json"). - Reply(http.StatusOK). - JSON(running) - // Run test - stopped, err := checkServiceHealth(context.Background()) - // Check error - assert.NoError(t, err) - assert.Empty(t, stopped) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("shows stopped container", func(t *testing.T) { - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/json"). - Reply(http.StatusOK). - JSON([]container.Summary{}) - // Run test - stopped, err := checkServiceHealth(context.Background()) - // Check error - assert.NoError(t, err) - assert.ElementsMatch(t, utils.GetDockerIds(), stopped) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on network error", func(t *testing.T) { - errNetwork := errors.New("network error") - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers/json"). - ReplyError(errNetwork) - // Run test - stopped, err := checkServiceHealth(context.Background()) - // Check error - assert.ErrorIs(t, err, errNetwork) - assert.Empty(t, stopped) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} - -func TestPrintStatus(t *testing.T) { - utils.Config.Db.Port = 0 - utils.Config.Hostname = "127.0.0.1" - utils.Config.Api.Enabled = false - utils.Config.Auth.Enabled = false - utils.Config.Storage.Enabled = false - utils.Config.Realtime.Enabled = false - utils.Config.Studio.Enabled = false - utils.Config.Analytics.Enabled = false - utils.Config.Inbucket.Enabled = false - - t.Run("outputs env var", func(t *testing.T) { - utils.Config.Hostname = "127.0.0.1" - // Run test - var stdout bytes.Buffer - assert.NoError(t, printStatus(CustomName{DbURL: "DB_URL"}, utils.OutputEnv, &stdout)) - // Check error - assert.Equal(t, "DB_URL=\"postgresql://postgres:postgres@127.0.0.1:0/postgres\"\n", stdout.String()) - }) - - t.Run("outputs json object", func(t *testing.T) { - // Run test - var stdout bytes.Buffer - assert.NoError(t, printStatus(CustomName{DbURL: "DB_URL"}, utils.OutputJson, &stdout)) - // Check error - assert.Equal(t, "{\n \"DB_URL\": \"postgresql://postgres:postgres@127.0.0.1:0/postgres\"\n}\n", stdout.String()) - }) - - t.Run("omits excluded services from json object", func(t *testing.T) { - utils.Config.Studio.Enabled = true - utils.Config.Studio.Port = 54323 - t.Cleanup(func() { - utils.Config.Studio.Enabled = false - }) - // Run test - var stdout bytes.Buffer - assert.NoError(t, printStatus(CustomName{DbURL: "DB_URL", StudioURL: "STUDIO_URL"}, utils.OutputJson, &stdout, utils.StudioId)) - // Check error - assert.Equal(t, "{\n \"DB_URL\": \"postgresql://postgres:postgres@127.0.0.1:0/postgres\"\n}\n", stdout.String()) - }) - - t.Run("outputs yaml properties", func(t *testing.T) { - // Run test - var stdout bytes.Buffer - assert.NoError(t, printStatus(CustomName{DbURL: "DB_URL"}, utils.OutputYaml, &stdout)) - // Check error - assert.Equal(t, "DB_URL: postgresql://postgres:postgres@127.0.0.1:0/postgres\n", stdout.String()) - }) - - t.Run("outputs toml fields", func(t *testing.T) { - // Run test - var stdout bytes.Buffer - assert.NoError(t, printStatus(CustomName{DbURL: "DB_URL"}, utils.OutputToml, &stdout)) - // Check error - assert.Equal(t, "DB_URL = \"postgresql://postgres:postgres@127.0.0.1:0/postgres\"\n", stdout.String()) - }) -} diff --git a/apps/cli-go/internal/storage/client/api.go b/apps/cli-go/internal/storage/client/api.go deleted file mode 100644 index 33b5d6711f..0000000000 --- a/apps/cli-go/internal/storage/client/api.go +++ /dev/null @@ -1,46 +0,0 @@ -package client - -import ( - "context" - "net/http" - - "github.com/spf13/viper" - "github.com/supabase/cli/internal/status" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/internal/utils/tenant" - "github.com/supabase/cli/pkg/fetcher" - "github.com/supabase/cli/pkg/storage" -) - -func NewStorageAPI(ctx context.Context, projectRef string) (storage.StorageAPI, error) { - client := storage.StorageAPI{} - if len(projectRef) == 0 { - client.Fetcher = newLocalClient() - } else if viper.IsSet("AUTH_SERVICE_ROLE_KEY") { - // Special case for calling storage API without personal access token - client.Fetcher = newRemoteClient(projectRef, utils.Config.Auth.ServiceRoleKey.Value) - } else if apiKey, err := tenant.GetApiKeys(ctx, projectRef); err == nil { - client.Fetcher = newRemoteClient(projectRef, apiKey.ServiceRole) - } else { - return client, err - } - return client, nil -} - -func newLocalClient() *fetcher.Fetcher { - return fetcher.NewServiceGateway( - utils.Config.Api.ExternalUrl, - utils.Config.Auth.ServiceRoleKey.Value, - fetcher.WithHTTPClient(status.NewKongClient()), - fetcher.WithUserAgent("SupabaseCLI/"+utils.Version), - ) -} - -func newRemoteClient(projectRef, token string) *fetcher.Fetcher { - return fetcher.NewServiceGateway( - "https://"+utils.GetSupabaseHost(projectRef), - token, - fetcher.WithHTTPClient(http.DefaultClient), - fetcher.WithUserAgent("SupabaseCLI/"+utils.Version), - ) -} diff --git a/apps/cli-go/internal/storage/client/scheme.go b/apps/cli-go/internal/storage/client/scheme.go deleted file mode 100644 index bb3254f5ba..0000000000 --- a/apps/cli-go/internal/storage/client/scheme.go +++ /dev/null @@ -1,38 +0,0 @@ -package client - -import ( - "net/url" - "strings" - - "github.com/go-errors/errors" -) - -const STORAGE_SCHEME = "ss" - -var ErrInvalidURL = errors.New("URL must match pattern ss:///bucket/[prefix]") - -func ParseStorageURL(objectURL string) (string, error) { - parsed, err := url.Parse(objectURL) - if err != nil { - return "", errors.Errorf("failed to parse storage url: %w", err) - } - if !strings.EqualFold(parsed.Scheme, STORAGE_SCHEME) || len(parsed.Path) == 0 || len(parsed.Host) > 0 { - return "", errors.New(ErrInvalidURL) - } - return parsed.Path, nil -} - -func SplitBucketPrefix(objectPath string) (string, string) { - if objectPath == "" || objectPath == "/" { - return "", "" - } - start := 0 - if objectPath[0] == '/' { - start = 1 - } - sep := strings.IndexByte(objectPath[start:], '/') - if sep < 0 { - return objectPath[start:], "" - } - return objectPath[start : sep+start], objectPath[sep+start+1:] -} diff --git a/apps/cli-go/internal/storage/client/scheme_test.go b/apps/cli-go/internal/storage/client/scheme_test.go deleted file mode 100644 index 5da6191a02..0000000000 --- a/apps/cli-go/internal/storage/client/scheme_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package client - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestParseStorageURL(t *testing.T) { - t.Run("parses valid url", func(t *testing.T) { - path, err := ParseStorageURL("ss:///bucket/folder/name.png") - assert.NoError(t, err) - assert.Equal(t, path, "/bucket/folder/name.png") - }) - - t.Run("throws error on invalid host", func(t *testing.T) { - path, err := ParseStorageURL("ss://bucket") - assert.ErrorIs(t, err, ErrInvalidURL) - assert.Empty(t, path) - }) - - t.Run("throws error on missing path", func(t *testing.T) { - path, err := ParseStorageURL("ss:") - assert.ErrorIs(t, err, ErrInvalidURL) - assert.Empty(t, path) - }) - - t.Run("throws error on invalid scheme", func(t *testing.T) { - path, err := ParseStorageURL(".") - assert.ErrorIs(t, err, ErrInvalidURL) - assert.Empty(t, path) - }) - - t.Run("throws error on invalid url", func(t *testing.T) { - path, err := ParseStorageURL(":") - assert.ErrorContains(t, err, "missing protocol scheme") - assert.Empty(t, path) - }) -} - -func TestSplitBucketPrefix(t *testing.T) { - t.Run("splits empty path", func(t *testing.T) { - bucket, prefix := SplitBucketPrefix("") - assert.Equal(t, bucket, "") - assert.Equal(t, prefix, "") - }) - - t.Run("splits root path", func(t *testing.T) { - bucket, prefix := SplitBucketPrefix("/") - assert.Equal(t, bucket, "") - assert.Equal(t, prefix, "") - }) - - t.Run("splits no slash", func(t *testing.T) { - bucket, prefix := SplitBucketPrefix("bucket") - assert.Equal(t, bucket, "bucket") - assert.Equal(t, prefix, "") - }) - - t.Run("splits prefix slash", func(t *testing.T) { - bucket, prefix := SplitBucketPrefix("/bucket") - assert.Equal(t, bucket, "bucket") - assert.Equal(t, prefix, "") - }) - - t.Run("splits suffix slash", func(t *testing.T) { - bucket, prefix := SplitBucketPrefix("bucket/") - assert.Equal(t, bucket, "bucket") - assert.Equal(t, prefix, "") - }) - - t.Run("splits file path", func(t *testing.T) { - bucket, prefix := SplitBucketPrefix("/bucket/folder/name.png") - assert.Equal(t, bucket, "bucket") - assert.Equal(t, prefix, "folder/name.png") - }) - - t.Run("splits dir path", func(t *testing.T) { - bucket, prefix := SplitBucketPrefix("/bucket/folder/") - assert.Equal(t, bucket, "bucket") - assert.Equal(t, prefix, "folder/") - }) -} diff --git a/apps/cli-go/internal/telemetry/client.go b/apps/cli-go/internal/telemetry/client.go deleted file mode 100644 index 717e4af181..0000000000 --- a/apps/cli-go/internal/telemetry/client.go +++ /dev/null @@ -1,149 +0,0 @@ -package telemetry - -import ( - "log" - "net/http" - "strings" - "time" - - "github.com/go-errors/errors" - "github.com/posthog/posthog-go" - "github.com/supabase/cli/internal/utils" -) - -type Analytics interface { - Enabled() bool - Capture(distinctID string, event string, properties map[string]any, groups map[string]string) error - Identify(distinctID string, properties map[string]any) error - Alias(distinctID string, alias string) error - GroupIdentify(groupType string, groupKey string, properties map[string]any) error - Close() error -} - -type queueClient interface { - Enqueue(posthog.Message) error - Close() error -} - -type constructor func(apiKey string, config posthog.Config) (queueClient, error) - -const shutdownTimeout = 2 * time.Second - -type Client struct { - client queueClient - baseProperties posthog.Properties -} - -func NewClient(apiKey string, endpoint string, baseProperties map[string]any, factory constructor) (*Client, error) { - if strings.TrimSpace(apiKey) == "" { - return &Client{baseProperties: makeProperties(baseProperties)}, nil - } - if factory == nil { - factory = func(apiKey string, config posthog.Config) (queueClient, error) { - return posthog.NewWithConfig(apiKey, config) - } - } - // Without a positive ShutdownTimeout, Close blocks indefinitely when the - // endpoint is unreachable, hanging every command on networks that block - // PostHog; keep it tight because blackholed networks pay the whole timeout - // at exit. The default logger prints delivery failures to stderr even for - // commands that succeeded, so route them to the --debug logger instead. - config := posthog.Config{ - ShutdownTimeout: shutdownTimeout, - Logger: posthog.StdLogger(log.New(utils.GetDebugLogger(), "posthog ", log.LstdFlags), true), - } - if endpoint != "" { - config.Endpoint = endpoint - } - // Preserve the active process-wide transport, which may be wrapped by debug.NewTransport() - // instead of assuming http.DefaultTransport is always a *http.Transport. - config.Transport = http.DefaultTransport - client, err := factory(apiKey, config) - if err != nil { - return nil, errors.Errorf("failed to initialize posthog client: %w", err) - } - return &Client{ - client: client, - baseProperties: makeProperties(baseProperties), - }, nil -} - -func (c *Client) Enabled() bool { - return c != nil && c.client != nil -} - -func (c *Client) Capture(distinctID string, event string, properties map[string]any, groups map[string]string) error { - if !c.Enabled() { - return nil - } - msg := posthog.Capture{ - DistinctId: distinctID, - Event: event, - Properties: c.properties(properties), - } - if len(groups) > 0 { - msg.Groups = makeGroups(groups) - } - return c.client.Enqueue(msg) -} - -func (c *Client) Identify(distinctID string, properties map[string]any) error { - if !c.Enabled() { - return nil - } - return c.client.Enqueue(posthog.Identify{ - DistinctId: distinctID, - Properties: c.properties(properties), - }) -} - -func (c *Client) Alias(distinctID string, alias string) error { - if !c.Enabled() { - return nil - } - return c.client.Enqueue(posthog.Alias{ - DistinctId: distinctID, - Alias: alias, - }) -} - -func (c *Client) GroupIdentify(groupType string, groupKey string, properties map[string]any) error { - if !c.Enabled() { - return nil - } - return c.client.Enqueue(posthog.GroupIdentify{ - Type: groupType, - Key: groupKey, - Properties: c.properties(properties), - }) -} - -func (c *Client) Close() error { - if !c.Enabled() { - return nil - } - return c.client.Close() -} - -func (c *Client) properties(properties map[string]any) posthog.Properties { - merged := posthog.NewProperties() - merged.Merge(c.baseProperties) - merged.Merge(makeProperties(properties)) - return merged -} - -func makeProperties(values map[string]any) posthog.Properties { - props := posthog.NewProperties() - for key, value := range values { - props.Set(key, value) - } - return props -} - -func makeGroups(values map[string]string) posthog.Groups { - groups := posthog.NewGroups() - for key, value := range values { - groups.Set(key, value) - } - return groups -} diff --git a/apps/cli-go/internal/telemetry/client_test.go b/apps/cli-go/internal/telemetry/client_test.go deleted file mode 100644 index bcc8db7d24..0000000000 --- a/apps/cli-go/internal/telemetry/client_test.go +++ /dev/null @@ -1,169 +0,0 @@ -package telemetry - -import ( - "io" - "net/http" - "os" - "testing" - "time" - - "github.com/posthog/posthog-go" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/debug" -) - -type fakeQueue struct { - messages []posthog.Message - closed bool -} - -func (f *fakeQueue) Enqueue(msg posthog.Message) error { - f.messages = append(f.messages, msg) - return nil -} - -func (f *fakeQueue) Close() error { - f.closed = true - return nil -} - -func TestNewClient(t *testing.T) { - t.Run("uses endpoint and enables analytics when key is set", func(t *testing.T) { - var gotKey string - var gotConfig posthog.Config - - client, err := NewClient("phc_test", "https://eu.i.posthog.com", map[string]any{"platform": "cli"}, func(apiKey string, config posthog.Config) (queueClient, error) { - gotKey = apiKey - gotConfig = config - return &fakeQueue{}, nil - }) - - require.NoError(t, err) - assert.True(t, client.Enabled()) - assert.Equal(t, "phc_test", gotKey) - assert.Equal(t, "https://eu.i.posthog.com", gotConfig.Endpoint) - assert.Equal(t, 2*time.Second, gotConfig.ShutdownTimeout) - assert.NotNil(t, gotConfig.Logger) - }) - - t.Run("becomes a no-op when key is empty", func(t *testing.T) { - client, err := NewClient("", "https://eu.i.posthog.com", map[string]any{"platform": "cli"}, func(apiKey string, config posthog.Config) (queueClient, error) { - t.Fatalf("constructor should not be called without an api key") - return nil, nil - }) - - require.NoError(t, err) - assert.False(t, client.Enabled()) - assert.NoError(t, client.Capture("device-1", EventCommandExecuted, map[string]any{"command": "login"}, nil)) - assert.NoError(t, client.Close()) - }) - t.Run("routes posthog logs to stderr only in debug mode", func(t *testing.T) { - originalStderr := os.Stderr - originalDebug := viper.GetBool("DEBUG") - reader, writer, err := os.Pipe() - require.NoError(t, err) - os.Stderr = writer - t.Cleanup(func() { - os.Stderr = originalStderr - viper.Set("DEBUG", originalDebug) - }) - - configFor := func(debugEnabled bool) posthog.Config { - viper.Set("DEBUG", debugEnabled) - var gotConfig posthog.Config - _, err := NewClient("phc_test", "", nil, func(apiKey string, config posthog.Config) (queueClient, error) { - gotConfig = config - return &fakeQueue{}, nil - }) - require.NoError(t, err) - return gotConfig - } - - configFor(false).Logger.Errorf("dropped %d messages", 1) - configFor(true).Logger.Errorf("dropped %d messages", 2) - - require.NoError(t, writer.Close()) - output, err := io.ReadAll(reader) - require.NoError(t, err) - assert.NotContains(t, string(output), "dropped 1 messages") - assert.Contains(t, string(output), "dropped 2 messages") - }) - t.Run("works when debug wraps the default transport", func(t *testing.T) { - original := http.DefaultTransport - http.DefaultTransport = debug.NewTransport() - t.Cleanup(func() { - http.DefaultTransport = original - }) - - client, err := NewClient("phc_test", "https://eu.i.posthog.com", map[string]any{"platform": "cli"}, nil) - - require.NoError(t, err) - require.NotNil(t, client) - assert.True(t, client.Enabled()) - assert.NoError(t, client.Close()) - }) -} - -func TestCaptureMergesBasePropertiesAndGroups(t *testing.T) { - queue := &fakeQueue{} - client, err := NewClient("phc_test", "https://eu.i.posthog.com", map[string]any{ - "platform": "cli", - "os": "darwin", - }, func(apiKey string, config posthog.Config) (queueClient, error) { - return queue, nil - }) - require.NoError(t, err) - - err = client.Capture("device-1", EventCommandExecuted, map[string]any{ - "command": "login", - }, map[string]string{ - GroupProject: "proj_123", - }) - - require.NoError(t, err) - require.Len(t, queue.messages, 1) - msg, ok := queue.messages[0].(posthog.Capture) - require.True(t, ok) - assert.Equal(t, "device-1", msg.DistinctId) - assert.Equal(t, EventCommandExecuted, msg.Event) - assert.Equal(t, "cli", msg.Properties["platform"]) - assert.Equal(t, "darwin", msg.Properties["os"]) - assert.Equal(t, "login", msg.Properties["command"]) - assert.Equal(t, posthog.Groups{GroupProject: "proj_123"}, msg.Groups) -} - -func TestIdentifyAliasAndGroupIdentify(t *testing.T) { - queue := &fakeQueue{} - client, err := NewClient("phc_test", "", map[string]any{"platform": "cli"}, func(apiKey string, config posthog.Config) (queueClient, error) { - return queue, nil - }) - require.NoError(t, err) - - require.NoError(t, client.Identify("user-123", map[string]any{"schema_version": 1})) - require.NoError(t, client.Alias("user-123", "device-123")) - require.NoError(t, client.GroupIdentify(GroupOrganization, "org_123", map[string]any{"slug": "acme"})) - require.NoError(t, client.Close()) - - require.Len(t, queue.messages, 3) - - identify, ok := queue.messages[0].(posthog.Identify) - require.True(t, ok) - assert.Equal(t, "user-123", identify.DistinctId) - assert.Equal(t, "cli", identify.Properties["platform"]) - assert.Equal(t, 1, identify.Properties["schema_version"]) - - alias, ok := queue.messages[1].(posthog.Alias) - require.True(t, ok) - assert.Equal(t, "user-123", alias.DistinctId) - assert.Equal(t, "device-123", alias.Alias) - - groupIdentify, ok := queue.messages[2].(posthog.GroupIdentify) - require.True(t, ok) - assert.Equal(t, GroupOrganization, groupIdentify.Type) - assert.Equal(t, "org_123", groupIdentify.Key) - assert.Equal(t, "cli", groupIdentify.Properties["platform"]) - assert.Equal(t, "acme", groupIdentify.Properties["slug"]) - assert.True(t, queue.closed) -} diff --git a/apps/cli-go/internal/telemetry/events.go b/apps/cli-go/internal/telemetry/events.go deleted file mode 100644 index 2ee362dc17..0000000000 --- a/apps/cli-go/internal/telemetry/events.go +++ /dev/null @@ -1,184 +0,0 @@ -package telemetry - -import "context" - -// CLI telemetry catalog. -// -// This file is the single place to review what analytics events the CLI sends -// and what metadata may be attached to them. Comments live next to the event, -// property, group, or signal definition they describe so the catalog is easy to -// scan without reading the rest of the implementation. -const ( - // - EventCommandExecuted: sent after a CLI command finishes, whether it - // succeeds or fails. This helps measure command usage, failure rates, and - // runtime. Event-specific properties are PropExitCode (process exit code), - // PropDurationMs (command runtime in milliseconds), and PropOutputFormat - // (the resolved output format the command used). Related groups: none - // added directly by this event. - EventCommandExecuted = "cli_command_executed" - // - EventProjectLinked: sent after the local CLI directory is linked to a - // Supabase project. This helps measure project-linking adoption and connect - // future events to the right project and organization. Event-specific - // properties: none. Related groups: GroupOrganization and GroupProject. - // Related group-identify payloads sent during linking are: - // organization group -> organization_slug, and project group -> name, - // organization_slug. - EventProjectLinked = "cli_project_linked" - // - EventLoginCompleted: sent after a login flow completes successfully. This - // helps measure successful login completion and supports identity stitching - // between anonymous and authenticated usage. Event-specific properties: - // none. Related groups: none added directly by this event. - EventLoginCompleted = "cli_login_completed" - // - EventStackStarted: sent after the local development stack starts - // successfully. This helps measure local development usage and successful - // stack startup. Event-specific properties: none. Related groups: none - // added directly by this event, but linked project groups may still be - // attached when available. - EventStackStarted = "cli_stack_started" - // - EventUpgradeSuggested: sent when a CLI command hits a plan-gated - // feature and displays a billing upgrade link. This helps identify - // which plan gates users encounter most often so we can improve - // error messages and documentation. Event-specific properties are - // PropFeatureKey (the entitlement key that was gated) and PropOrgSlug - // (the organization slug, empty if lookup failed). - EventUpgradeSuggested = "cli_upgrade_suggested" -) - -// Properties specific to EventUpgradeSuggested. -const ( - // PropFeatureKey is the entitlement key that triggered the upgrade suggestion. - PropFeatureKey = "feature_key" - // PropOrgSlug is the organization slug associated with the project. - PropOrgSlug = "org_slug" -) - -// TrackUpgradeSuggested fires an EventUpgradeSuggested telemetry event. -// Safe to call with any context; no-ops when telemetry is not configured. -func TrackUpgradeSuggested(ctx context.Context, featureKey, orgSlug string) { - if svc := FromContext(ctx); svc != nil { - _ = svc.Capture(ctx, EventUpgradeSuggested, map[string]any{ - PropFeatureKey: featureKey, - PropOrgSlug: orgSlug, - }, nil) - } -} - -// Shared event properties added to every captured event by Service.Capture. -const ( - // PropPlatform identifies the product source for the event. The CLI always - // sends "cli". - PropPlatform = "platform" - // PropSchemaVersion is the version of the telemetry payload format. This is - // not a database schema version. - PropSchemaVersion = "schema_version" - // PropDeviceID is an anonymous identifier for this CLI installation on this - // machine. - PropDeviceID = "device_id" - // PropSessionID is the PostHog session identifier used to group activity from - // one CLI session together. - PropSessionID = "$session_id" - // PropIsFirstRun is true when the current telemetry state was created during - // this run, which helps distinguish first-time setup from repeat usage. - PropIsFirstRun = "is_first_run" - // PropIsTTY is true when stdout is attached to an interactive terminal. - PropIsTTY = "is_tty" - // PropIsCI is true when the CLI appears to be running in a CI environment. - PropIsCI = "is_ci" - // PropIsAgent is true when the CLI appears to be running under an AI agent or - // automation tool. - PropIsAgent = "is_agent" - // PropOS is the operating system reported by the Go runtime. - PropOS = "os" - // PropArch is the CPU architecture reported by the Go runtime. - PropArch = "arch" - // PropCLIVersion is the version string of the CLI build that sent the event. - PropCLIVersion = "cli_version" - // PropEnvSignals is an optional summary of coarse environment hints. It is - // not a raw dump of environment variables. - PropEnvSignals = "env_signals" - // PropCommandRunID identifies one command invocation and can be used to tie - // together telemetry emitted during a single command run. - PropCommandRunID = "command_run_id" - // PropCommand is the normalized command path, such as "link" or "db push". - PropCommand = "command" - // PropFlags contains changed CLI flags for that command run. Safe flag values - // may be included, while sensitive values are redacted in the command - // telemetry implementation. - PropFlags = "flags" - // PropExitCode is the process exit code for the command that produced the - // event. - PropExitCode = "exit_code" - // PropDurationMs is the command runtime in milliseconds. - PropDurationMs = "duration_ms" - // PropOutputFormat is the resolved output format the command used (for - // example "pretty", "json", "yaml"), after agent auto-detection and any - // command-local override are applied. - PropOutputFormat = "output_format" -) - -// Group identifiers associate events with higher-level entities in PostHog. -const ( - // GroupOrganization identifies the Supabase organization related to the - // event. - GroupOrganization = "organization" - // GroupProject identifies the Supabase project related to the event. - GroupProject = "project" -) - -var ( - // EnvSignalPresenceKeys lists environment variables whose presence is recorded - // as true inside the "env_signals" property. - EnvSignalPresenceKeys = [...]string{ - // AI tools signals - "CURSOR_AGENT", - "CURSOR_TRACE_ID", - "GEMINI_CLI", - "CODEX_SANDBOX", - "CODEX_CI", - "CODEX_THREAD_ID", - "ANTIGRAVITY_AGENT", - "AUGMENT_AGENT", - "OPENCODE_CLIENT", - "CLAUDECODE", - "CLAUDE_CODE", - "REPL_ID", - "COPILOT_MODEL", - "COPILOT_ALLOW_ALL", - "COPILOT_GITHUB_TOKEN", - // CI signals - "CI", - "GITHUB_ACTIONS", - "BUILDKITE", - "TF_BUILD", - "JENKINS_URL", - "GITLAB_CI", - // Extra signals - "GITHUB_TOKEN", - "GITHUB_HEAD_REF", - "BITBUCKET_CLONE_DIR", - // Supabase environment signals - "SUPABASE_ACCESS_TOKEN", - "SUPABASE_HOME", - "SYSTEMROOT", - "SUPABASE_SSL_DEBUG", - "SUPABASE_CA_SKIP_VERIFY", - "SSL_CERT_FILE", - "SSL_CERT_DIR", - "NPM_CONFIG_REGISTRY", - "SUPABASE_SERVICE_ROLE_KEY", - "SUPABASE_PROJECT_ID", - "SUPABASE_POSTGRES_URL", - "SUPABASE_ENV", - } - - // EnvSignalValueKeys lists environment variables whose trimmed values may be - // recorded inside the "env_signals" property. - EnvSignalValueKeys = [...]string{ - "AI_AGENT", - "CURSOR_EXTENSION_HOST_ROLE", - "TERM", - "TERM_PROGRAM", - "TERM_PROGRAM_VERSION", - "TERM_COLOR_MODE", - } -) diff --git a/apps/cli-go/internal/telemetry/project.go b/apps/cli-go/internal/telemetry/project.go deleted file mode 100644 index 8fe77a2096..0000000000 --- a/apps/cli-go/internal/telemetry/project.go +++ /dev/null @@ -1,125 +0,0 @@ -package telemetry - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - - "github.com/go-errors/errors" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/config" -) - -type LinkedProject struct { - Ref string `json:"ref"` - Name string `json:"name"` - OrganizationID string `json:"organization_id"` - OrganizationSlug string `json:"organization_slug"` -} - -// linkedProjectEnv carries the linked project as the JSON of LinkedProject when the TypeScript -// CLI keeps the link outside supabase/.temp. -const linkedProjectEnv = "SUPABASE_INTERNAL_LINKED_PROJECT" - -func linkedProjectPath() string { - return filepath.Join(utils.TempDir, "linked-project.json") -} - -func SaveLinkedProject(project api.V1ProjectWithDatabaseResponseOutput, fsys afero.Fs) error { - linked := LinkedProject{ - Ref: project.Ref, - Name: project.Name, - OrganizationID: project.OrganizationId, - OrganizationSlug: project.OrganizationSlug, - } - contents, err := json.Marshal(linked) - if err != nil { - return errors.Errorf("failed to encode linked project: %w", err) - } - return utils.WriteFile(linkedProjectPath(), contents, fsys) -} - -func LoadLinkedProject(fsys afero.Fs) (LinkedProject, error) { - if config.UsesStackState() { - contents := os.Getenv(linkedProjectEnv) - if contents == "" { - return LinkedProject{}, os.ErrNotExist - } - return parseLinkedProject([]byte(contents)) - } - contents, err := afero.ReadFile(fsys, linkedProjectPath()) - if err != nil { - return LinkedProject{}, err - } - return parseLinkedProject(contents) -} - -func parseLinkedProject(contents []byte) (LinkedProject, error) { - var linked LinkedProject - if err := json.Unmarshal(contents, &linked); err != nil { - return LinkedProject{}, errors.Errorf("failed to parse linked project: %w", err) - } - return linked, nil -} - -// HasLinkedProject reports whether a cached linked-project.json exists. -func HasLinkedProject(fsys afero.Fs) bool { - _, err := LoadLinkedProject(fsys) - return err == nil -} - -// CacheProjectAndIdentifyGroups writes project metadata to linked-project.json -// and fires GroupIdentify for the org and project so PostHog has group metadata. -// This matches the behavior of the `supabase link` flow. -// -// The caller is responsible for fetching the project from the API and checking -// auth — this function only handles caching and PostHog group identification. -// -// Best-effort: logs errors to debug output, never returns them. -func CacheProjectAndIdentifyGroups(project api.V1ProjectWithDatabaseResponseOutput, service *Service, fsys afero.Fs) { - if err := SaveLinkedProject(project, fsys); err != nil { - fmt.Fprintln(utils.GetDebugLogger(), err) - } - if service == nil { - return - } - if project.OrganizationId != "" { - if err := service.GroupIdentify(GroupOrganization, project.OrganizationId, map[string]any{ - "organization_slug": project.OrganizationSlug, - }); err != nil { - fmt.Fprintln(utils.GetDebugLogger(), err) - } - } - if project.Ref != "" { - if err := service.GroupIdentify(GroupProject, project.Ref, map[string]any{ - "name": project.Name, - "organization_slug": project.OrganizationSlug, - }); err != nil { - fmt.Fprintln(utils.GetDebugLogger(), err) - } - } -} - -func linkedProjectGroups(fsys afero.Fs) map[string]string { - linked, err := LoadLinkedProject(fsys) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil - } - return nil - } - groups := make(map[string]string, 2) - if linked.OrganizationID != "" { - groups[GroupOrganization] = linked.OrganizationID - } - if linked.Ref != "" { - groups[GroupProject] = linked.Ref - } - if len(groups) == 0 { - return nil - } - return groups -} diff --git a/apps/cli-go/internal/telemetry/project_test.go b/apps/cli-go/internal/telemetry/project_test.go deleted file mode 100644 index 2cf1fa89ce..0000000000 --- a/apps/cli-go/internal/telemetry/project_test.go +++ /dev/null @@ -1,151 +0,0 @@ -package telemetry - -import ( - "os" - "testing" - "time" - - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/config" -) - -var testProject = api.V1ProjectWithDatabaseResponseOutput{ - Ref: "proj_abc", - Name: "My Project", - OrganizationId: "org_123", - OrganizationSlug: "acme", -} - -func newTestService(t *testing.T, fsys afero.Fs, analytics *fakeAnalytics) *Service { - t.Helper() - service, err := NewService(fsys, Options{ - Analytics: analytics, - Now: func() time.Time { return time.Date(2026, time.April, 15, 12, 0, 0, 0, time.UTC) }, - }) - require.NoError(t, err) - return service -} - -func TestHasLinkedProject(t *testing.T) { - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - - t.Run("false when no cache", func(t *testing.T) { - fsys := afero.NewMemMapFs() - assert.False(t, HasLinkedProject(fsys)) - }) - - t.Run("true when cache exists", func(t *testing.T) { - fsys := afero.NewMemMapFs() - require.NoError(t, SaveLinkedProject(testProject, fsys)) - assert.True(t, HasLinkedProject(fsys)) - }) -} - -func TestCacheProjectAndIdentifyGroups(t *testing.T) { - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - - t.Run("writes cache file", func(t *testing.T) { - fsys := afero.NewMemMapFs() - CacheProjectAndIdentifyGroups(testProject, nil, fsys) - - linked, err := LoadLinkedProject(fsys) - require.NoError(t, err) - assert.Equal(t, "proj_abc", linked.Ref) - assert.Equal(t, "org_123", linked.OrganizationID) - assert.Equal(t, "acme", linked.OrganizationSlug) - assert.Equal(t, "My Project", linked.Name) - }) - - t.Run("fires GroupIdentify for org and project", func(t *testing.T) { - fsys := afero.NewMemMapFs() - analytics := &fakeAnalytics{enabled: true} - service := newTestService(t, fsys, analytics) - - CacheProjectAndIdentifyGroups(testProject, service, fsys) - - require.Len(t, analytics.groupIdentifies, 2) - - orgCall := analytics.groupIdentifies[0] - assert.Equal(t, GroupOrganization, orgCall.groupType) - assert.Equal(t, "org_123", orgCall.groupKey) - assert.Equal(t, "acme", orgCall.properties["organization_slug"]) - - projCall := analytics.groupIdentifies[1] - assert.Equal(t, GroupProject, projCall.groupType) - assert.Equal(t, "proj_abc", projCall.groupKey) - assert.Equal(t, "My Project", projCall.properties["name"]) - assert.Equal(t, "acme", projCall.properties["organization_slug"]) - }) - - t.Run("skips GroupIdentify when service is nil", func(t *testing.T) { - fsys := afero.NewMemMapFs() - CacheProjectAndIdentifyGroups(testProject, nil, fsys) - - // Cache should still be written - linked, err := LoadLinkedProject(fsys) - require.NoError(t, err) - assert.Equal(t, "proj_abc", linked.Ref) - }) - - t.Run("skips GroupIdentify for empty org ID", func(t *testing.T) { - fsys := afero.NewMemMapFs() - analytics := &fakeAnalytics{enabled: true} - service := newTestService(t, fsys, analytics) - - noOrgProject := api.V1ProjectWithDatabaseResponseOutput{ - Ref: "proj_abc", - Name: "My Project", - } - CacheProjectAndIdentifyGroups(noOrgProject, service, fsys) - - // Only project GroupIdentify, no org - require.Len(t, analytics.groupIdentifies, 1) - assert.Equal(t, GroupProject, analytics.groupIdentifies[0].groupType) - }) -} - -func TestLinkedProjectGroups(t *testing.T) { - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - - t.Run("returns nil when no cache", func(t *testing.T) { - fsys := afero.NewMemMapFs() - groups := linkedProjectGroups(fsys) - assert.Nil(t, groups) - }) - - t.Run("returns groups from cache", func(t *testing.T) { - fsys := afero.NewMemMapFs() - require.NoError(t, SaveLinkedProject(testProject, fsys)) - groups := linkedProjectGroups(fsys) - assert.Equal(t, map[string]string{ - GroupOrganization: "org_123", - GroupProject: "proj_abc", - }, groups) - }) - - t.Run("reads the linked project from the env in stack mode", func(t *testing.T) { - t.Setenv(config.StackStateEnv, "1") - t.Setenv(linkedProjectEnv, `{"ref":"proj_abc","name":"n","organization_id":"org_123","organization_slug":"acme"}`) - fsys := afero.NewMemMapFs() - // The legacy cache must stay unread. - require.NoError(t, SaveLinkedProject(api.V1ProjectWithDatabaseResponseOutput{Ref: "stale", OrganizationId: "stale"}, fsys)) - assert.Equal(t, map[string]string{ - GroupOrganization: "org_123", - GroupProject: "proj_abc", - }, linkedProjectGroups(fsys)) - assert.True(t, HasLinkedProject(fsys)) - }) - - t.Run("reads as unlinked in stack mode without the env", func(t *testing.T) { - t.Setenv(config.StackStateEnv, "1") - t.Setenv(linkedProjectEnv, "") - fsys := afero.NewMemMapFs() - require.NoError(t, SaveLinkedProject(testProject, fsys)) - _, err := LoadLinkedProject(fsys) - assert.ErrorIs(t, err, os.ErrNotExist) - assert.Nil(t, linkedProjectGroups(fsys)) - }) -} diff --git a/apps/cli-go/internal/telemetry/service.go b/apps/cli-go/internal/telemetry/service.go deleted file mode 100644 index ced8a8ee01..0000000000 --- a/apps/cli-go/internal/telemetry/service.go +++ /dev/null @@ -1,294 +0,0 @@ -package telemetry - -import ( - "context" - "os" - "runtime" - "time" - - "github.com/google/uuid" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/utils" -) - -type commandContextKey struct{} -type serviceContextKey struct{} - -type CommandContext struct { - RunID string - Command string - Flags map[string]any - Groups map[string]string -} - -type Options struct { - Analytics Analytics - Now func() time.Time - IsTTY bool - IsCI bool - IsAgent bool - EnvSignals map[string]any - CLIName string - GOOS string - GOARCH string -} - -type Service struct { - fsys afero.Fs - analytics Analytics - now func() time.Time - state State - userID string - isFirstRun bool - isTTY bool - isCI bool - isAgent bool - envSignals map[string]any - cliVersion string - goos string - goarch string -} - -func NewService(fsys afero.Fs, opts Options) (*Service, error) { - now := opts.Now - if now == nil { - now = time.Now - } - state, created, err := LoadOrCreateState(fsys, now()) - if err != nil { - return nil, err - } - analytics := opts.Analytics - if analytics == nil { - analytics, err = NewClient(utils.PostHogAPIKey, utils.PostHogEndpoint, nil, nil) - if err != nil { - return nil, err - } - } - cliVersion := opts.CLIName - if cliVersion == "" { - cliVersion = utils.Version - } - goos := opts.GOOS - if goos == "" { - goos = runtime.GOOS - } - goarch := opts.GOARCH - if goarch == "" { - goarch = runtime.GOARCH - } - return &Service{ - fsys: fsys, - analytics: analytics, - now: now, - state: state, - isFirstRun: created, - isTTY: opts.IsTTY, - isCI: opts.IsCI, - isAgent: opts.IsAgent, - envSignals: opts.EnvSignals, - cliVersion: cliVersion, - goos: goos, - goarch: goarch, - }, nil -} - -func WithCommandContext(ctx context.Context, cmd CommandContext) context.Context { - return context.WithValue(ctx, commandContextKey{}, cmd) -} - -func WithService(ctx context.Context, service *Service) context.Context { - return context.WithValue(ctx, serviceContextKey{}, service) -} - -func FromContext(ctx context.Context) *Service { - if ctx == nil { - return nil - } - service, _ := ctx.Value(serviceContextKey{}).(*Service) - return service -} - -// Property catalog: see events.go. -func (s *Service) Capture(ctx context.Context, event string, properties map[string]any, groups map[string]string) error { - if !s.canSend() { - return nil - } - mergedProperties := s.baseProperties() - command := commandContextFrom(ctx) - if command.RunID != "" { - mergedProperties[PropCommandRunID] = command.RunID - } - if command.Command != "" { - mergedProperties[PropCommand] = command.Command - } - if command.Flags != nil { - mergedProperties[PropFlags] = command.Flags - } - for key, value := range properties { - mergedProperties[key] = value - } - return s.analytics.Capture(s.distinctID(), event, mergedProperties, mergeGroups(linkedProjectGroups(s.fsys), mergeGroups(command.Groups, groups))) -} - -// StitchLogin records the authenticated user as the identity for all -// subsequent captures in this process. In persistent runtimes it also merges -// the device's pre-login history via $create_alias and persists the identity -// for future runs; ephemeral runtimes get the in-memory stamp only. -// See docs/adr/0013-hybrid-stitch-stamp-identity-attribution.md. -func (s *Service) StitchLogin(distinctID string) error { - if s == nil { - return nil - } - // Alias only the first identity this device ever sees. Re-aliasing on - // re-login (or on the login command's call after the response hook has - // already stitched) would merge a second user into the device's existing - // person graph in PostHog. - firstIdentity := s.state.DistinctID == "" - s.userID = distinctID - if s.isEphemeralIdentityRuntime() { - return nil - } - if firstIdentity && s.canSend() { - if err := s.analytics.Alias(distinctID, s.state.DeviceID); err != nil { - // Leave the identity re-stitchable without dropping the in-memory - // stamp: nothing was enqueued, so a retry (e.g. the login command - // after the response hook errored) must still qualify as the first - // identity, but captures in this process should remain attributed. - return err - } - } - s.state.DistinctID = distinctID - return SaveState(s.state, s.fsys) -} - -// ObserveAuthenticatedUser records who the current process is authenticated -// as. First-time identities get the full stitch; when an identity already -// exists (e.g. telemetry.json holds a previous user but the active token -// belongs to another), only the in-memory stamp is updated — re-aliasing the -// device to a second user would merge unrelated person graphs in PostHog. -func (s *Service) ObserveAuthenticatedUser(distinctID string) error { - if s == nil { - return nil - } - if s.NeedsIdentityStitch() { - return s.StitchLogin(distinctID) - } - s.userID = distinctID - return nil -} - -// ResetIdentity severs the link between this device and the logged-out user: -// the identity is forgotten and the device ID is rotated, so a later login as -// a different account aliases a fresh device instead of one already merged -// into the previous user's person graph. Logout-only — transient failure -// paths use ClearDistinctID, which keeps the device ID. -func (s *Service) ResetIdentity() error { - if s == nil { - return nil - } - s.userID = "" - s.state.DistinctID = "" - s.state.DeviceID = uuid.NewString() - return SaveState(s.state, s.fsys) -} - -func (s *Service) ClearDistinctID() error { - if s == nil { - return nil - } - s.userID = "" - s.state.DistinctID = "" - return SaveState(s.state, s.fsys) -} - -func (s *Service) NeedsIdentityStitch() bool { - return s != nil && s.userID == "" && s.state.DistinctID == "" && s.canSend() -} - -func (s *Service) isEphemeralIdentityRuntime() bool { - return s.isCI || (s.isFirstRun && !s.isTTY) -} - -func (s *Service) GroupIdentify(groupType string, groupKey string, properties map[string]any) error { - if !s.canSend() { - return nil - } - return s.analytics.GroupIdentify(groupType, groupKey, s.basePropertiesWith(properties)) -} - -func (s *Service) Close() error { - if s == nil || s.analytics == nil { - return nil - } - return s.analytics.Close() -} - -func (s *Service) baseProperties() map[string]any { - properties := map[string]any{ - PropPlatform: "cli", - PropSchemaVersion: s.state.SchemaVersion, - PropDeviceID: s.state.DeviceID, - PropSessionID: s.state.SessionID, - PropIsFirstRun: s.isFirstRun, - PropIsTTY: s.isTTY, - PropIsCI: s.isCI, - PropIsAgent: s.isAgent, - PropOS: s.goos, - PropArch: s.goarch, - PropCLIVersion: s.cliVersion, - } - if len(s.envSignals) > 0 { - properties[PropEnvSignals] = s.envSignals - } - return properties -} - -func (s *Service) basePropertiesWith(properties map[string]any) map[string]any { - merged := s.baseProperties() - for key, value := range properties { - merged[key] = value - } - return merged -} - -func (s *Service) distinctID() string { - if s.userID != "" { - return s.userID - } - if s.state.DistinctID != "" { - return s.state.DistinctID - } - return s.state.DeviceID -} - -func commandContextFrom(ctx context.Context) CommandContext { - if ctx == nil { - return CommandContext{} - } - cmd, _ := ctx.Value(commandContextKey{}).(CommandContext) - return cmd -} - -func mergeGroups(existing map[string]string, extra map[string]string) map[string]string { - if len(existing) == 0 && len(extra) == 0 { - return nil - } - merged := make(map[string]string, len(existing)+len(extra)) - for key, value := range existing { - merged[key] = value - } - for key, value := range extra { - merged[key] = value - } - return merged -} - -func (s *Service) canSend() bool { - return s != nil && - s.analytics != nil && - s.analytics.Enabled() && - s.state.Enabled && - os.Getenv("DO_NOT_TRACK") != "1" && - os.Getenv("SUPABASE_TELEMETRY_DISABLED") != "1" -} diff --git a/apps/cli-go/internal/telemetry/service_test.go b/apps/cli-go/internal/telemetry/service_test.go deleted file mode 100644 index e2fe7b2fda..0000000000 --- a/apps/cli-go/internal/telemetry/service_test.go +++ /dev/null @@ -1,570 +0,0 @@ -package telemetry - -import ( - "context" - "testing" - "time" - - "github.com/google/uuid" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/pkg/api" -) - -type captureCall struct { - distinctID string - event string - properties map[string]any - groups map[string]string -} - -type identifyCall struct { - distinctID string - properties map[string]any -} - -type aliasCall struct { - distinctID string - alias string -} - -type groupIdentifyCall struct { - groupType string - groupKey string - properties map[string]any -} - -type fakeAnalytics struct { - enabled bool - captures []captureCall - identifies []identifyCall - aliases []aliasCall - groupIdentifies []groupIdentifyCall - aliasErr error - closed bool -} - -func (f *fakeAnalytics) Enabled() bool { return f.enabled } - -func (f *fakeAnalytics) Capture(distinctID string, event string, properties map[string]any, groups map[string]string) error { - f.captures = append(f.captures, captureCall{distinctID: distinctID, event: event, properties: properties, groups: groups}) - return nil -} - -func (f *fakeAnalytics) Identify(distinctID string, properties map[string]any) error { - f.identifies = append(f.identifies, identifyCall{distinctID: distinctID, properties: properties}) - return nil -} - -func (f *fakeAnalytics) Alias(distinctID string, alias string) error { - if f.aliasErr != nil { - err := f.aliasErr - f.aliasErr = nil - return err - } - f.aliases = append(f.aliases, aliasCall{distinctID: distinctID, alias: alias}) - return nil -} - -func (f *fakeAnalytics) GroupIdentify(groupType string, groupKey string, properties map[string]any) error { - f.groupIdentifies = append(f.groupIdentifies, groupIdentifyCall{groupType: groupType, groupKey: groupKey, properties: properties}) - return nil -} - -func (f *fakeAnalytics) Close() error { - f.closed = true - return nil -} - -func TestServiceCaptureIncludesBasePropertiesAndCommandContext(t *testing.T) { - now := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC) - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - analytics := &fakeAnalytics{enabled: true} - - service, err := NewService(fsys, Options{ - Analytics: analytics, - Now: func() time.Time { return now }, - IsTTY: true, - IsCI: true, - IsAgent: true, - EnvSignals: map[string]any{ - "CLAUDE_CODE": true, - "TERM_PROGRAM": "iTerm.app", - }, - CLIName: "1.2.3", - GOOS: "darwin", - GOARCH: "arm64", - }) - require.NoError(t, err) - - ctx := WithCommandContext(context.Background(), CommandContext{ - RunID: "run-123", - Command: "login", - Flags: map[string]any{ - "token": "", - }, - }) - - require.NoError(t, service.Capture(ctx, EventCommandExecuted, map[string]any{ - PropDurationMs: 42, - }, nil)) - - require.Len(t, analytics.captures, 1) - call := analytics.captures[0] - assert.NoError(t, uuid.Validate(call.distinctID)) - assert.Equal(t, EventCommandExecuted, call.event) - assert.Equal(t, "cli", call.properties[PropPlatform]) - assert.Equal(t, SchemaVersion, call.properties[PropSchemaVersion]) - assert.Equal(t, true, call.properties[PropIsFirstRun]) - assert.Equal(t, true, call.properties[PropIsTTY]) - assert.Equal(t, true, call.properties[PropIsCI]) - assert.Equal(t, true, call.properties[PropIsAgent]) - assert.Equal(t, map[string]any{ - "CLAUDE_CODE": true, - "TERM_PROGRAM": "iTerm.app", - }, call.properties[PropEnvSignals]) - assert.Equal(t, "darwin", call.properties[PropOS]) - assert.Equal(t, "arm64", call.properties[PropArch]) - assert.Equal(t, "1.2.3", call.properties[PropCLIVersion]) - assert.Equal(t, "run-123", call.properties[PropCommandRunID]) - assert.Equal(t, "login", call.properties[PropCommand]) - assert.Equal(t, map[string]any{"token": ""}, call.properties[PropFlags]) - _, hasFlagsUsed := call.properties["flags_used"] - assert.False(t, hasFlagsUsed) - _, hasFlagValues := call.properties["flag_values"] - assert.False(t, hasFlagValues) - assert.Equal(t, 42, call.properties[PropDurationMs]) -} - -func TestServiceStitchLoginPersistsDistinctID(t *testing.T) { - now := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC) - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - analytics := &fakeAnalytics{enabled: true} - - service, err := NewService(fsys, Options{ - Analytics: analytics, - Now: func() time.Time { return now }, - IsTTY: true, - }) - require.NoError(t, err) - deviceID := service.state.DeviceID - - require.NoError(t, service.StitchLogin("user-123")) - require.NoError(t, service.Capture(context.Background(), EventLoginCompleted, nil, nil)) - - require.Len(t, analytics.aliases, 1) - assert.Equal(t, "user-123", analytics.aliases[0].distinctID) - assert.Equal(t, deviceID, analytics.aliases[0].alias) - assert.Empty(t, analytics.identifies) - require.Len(t, analytics.captures, 1) - assert.Equal(t, "user-123", analytics.captures[0].distinctID) - - state, err := LoadState(fsys) - require.NoError(t, err) - assert.Equal(t, "user-123", state.DistinctID) -} - -func TestServiceStitchLoginInEphemeralRuntimeStampsWithoutPersisting(t *testing.T) { - now := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC) - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - analytics := &fakeAnalytics{enabled: true} - - service, err := NewService(fsys, Options{ - Analytics: analytics, - Now: func() time.Time { return now }, - IsCI: true, - }) - require.NoError(t, err) - - require.NoError(t, service.StitchLogin("user-123")) - require.NoError(t, service.Capture(context.Background(), EventCommandExecuted, nil, nil)) - - require.Len(t, analytics.captures, 1) - assert.Equal(t, "user-123", analytics.captures[0].distinctID) - assert.Empty(t, analytics.aliases) - assert.Empty(t, analytics.identifies) - - state, err := LoadState(fsys) - require.NoError(t, err) - assert.Empty(t, state.DistinctID) -} - -func TestServiceObserveAuthenticatedUser(t *testing.T) { - now := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC) - - t.Run("stamps over a stale persisted identity without alias or state write", func(t *testing.T) { - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - analytics := &fakeAnalytics{enabled: true} - require.NoError(t, SaveState(State{ - Enabled: true, - DeviceID: uuid.NewString(), - SessionID: uuid.NewString(), - SessionLastActive: now, - SchemaVersion: SchemaVersion, - DistinctID: "old-user", - }, fsys)) - - service, err := NewService(fsys, Options{ - Analytics: analytics, - Now: func() time.Time { return now }, - IsTTY: true, - }) - require.NoError(t, err) - - require.NoError(t, service.ObserveAuthenticatedUser("new-user")) - require.NoError(t, service.Capture(context.Background(), EventCommandExecuted, nil, nil)) - - assert.Empty(t, analytics.aliases) - assert.Empty(t, analytics.identifies) - require.Len(t, analytics.captures, 1) - assert.Equal(t, "new-user", analytics.captures[0].distinctID) - - state, err := LoadState(fsys) - require.NoError(t, err) - assert.Equal(t, "old-user", state.DistinctID) - }) - - t.Run("performs the full stitch when no identity exists yet", func(t *testing.T) { - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - analytics := &fakeAnalytics{enabled: true} - - service, err := NewService(fsys, Options{ - Analytics: analytics, - Now: func() time.Time { return now }, - IsTTY: true, - }) - require.NoError(t, err) - - require.NoError(t, service.ObserveAuthenticatedUser("user-123")) - - require.Len(t, analytics.aliases, 1) - assert.Equal(t, "user-123", analytics.aliases[0].distinctID) - state, err := LoadState(fsys) - require.NoError(t, err) - assert.Equal(t, "user-123", state.DistinctID) - }) -} - -func TestServiceStitchLoginReloginDoesNotRealias(t *testing.T) { - now := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC) - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - analytics := &fakeAnalytics{enabled: true} - require.NoError(t, SaveState(State{ - Enabled: true, - DeviceID: uuid.NewString(), - SessionID: uuid.NewString(), - SessionLastActive: now, - SchemaVersion: SchemaVersion, - DistinctID: "user-a", - }, fsys)) - - service, err := NewService(fsys, Options{ - Analytics: analytics, - Now: func() time.Time { return now }, - IsTTY: true, - }) - require.NoError(t, err) - - require.NoError(t, service.StitchLogin("user-b")) - - assert.Empty(t, analytics.aliases) - state, err := LoadState(fsys) - require.NoError(t, err) - assert.Equal(t, "user-b", state.DistinctID) - - require.NoError(t, service.Capture(context.Background(), EventLoginCompleted, nil, nil)) - require.Len(t, analytics.captures, 1) - assert.Equal(t, "user-b", analytics.captures[0].distinctID) -} - -func TestServiceStitchLoginIsIdempotentWithinProcess(t *testing.T) { - now := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC) - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - analytics := &fakeAnalytics{enabled: true} - - service, err := NewService(fsys, Options{ - Analytics: analytics, - Now: func() time.Time { return now }, - IsTTY: true, - }) - require.NoError(t, err) - - // The response hook stitches first; the login command then calls - // StitchLogin directly with the same id. One alias total. - require.NoError(t, service.ObserveAuthenticatedUser("user-123")) - require.NoError(t, service.StitchLogin("user-123")) - - require.Len(t, analytics.aliases, 1) -} - -func TestServiceResetIdentityRotatesDeviceID(t *testing.T) { - now := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC) - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - analytics := &fakeAnalytics{enabled: true} - - service, err := NewService(fsys, Options{ - Analytics: analytics, - Now: func() time.Time { return now }, - IsTTY: true, - }) - require.NoError(t, err) - require.NoError(t, service.StitchLogin("user-a")) - require.Len(t, analytics.aliases, 1) - oldDeviceID := analytics.aliases[0].alias - - require.NoError(t, service.ResetIdentity()) - - state, err := LoadState(fsys) - require.NoError(t, err) - assert.Empty(t, state.DistinctID) - assert.NotEqual(t, oldDeviceID, state.DeviceID) - assert.NoError(t, uuid.Validate(state.DeviceID)) - - // A later login as another user aliases the fresh device id, so the old - // user's person graph is never touched. - require.NoError(t, service.StitchLogin("user-b")) - require.Len(t, analytics.aliases, 2) - assert.Equal(t, state.DeviceID, analytics.aliases[1].alias) - - require.NoError(t, service.Capture(context.Background(), EventCommandExecuted, nil, nil)) - assert.Equal(t, "user-b", analytics.captures[len(analytics.captures)-1].distinctID) -} - -func TestServiceStitchLoginRetriesAliasAfterEnqueueFailure(t *testing.T) { - now := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC) - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - analytics := &fakeAnalytics{enabled: true, aliasErr: assert.AnError} - - service, err := NewService(fsys, Options{ - Analytics: analytics, - Now: func() time.Time { return now }, - IsTTY: true, - }) - require.NoError(t, err) - - require.Error(t, service.StitchLogin("user-123")) - require.NoError(t, service.Capture(context.Background(), EventLoginCompleted, nil, nil)) - require.Len(t, analytics.captures, 1) - assert.Equal(t, "user-123", analytics.captures[0].distinctID) - state, err := LoadState(fsys) - require.NoError(t, err) - assert.Empty(t, state.DistinctID) - - // The failed attempt must not poison the first-identity gate: a retry - // (e.g. the login command after the response hook errored) still aliases. - require.NoError(t, service.StitchLogin("user-123")) - require.Len(t, analytics.aliases, 1) - state, err = LoadState(fsys) - require.NoError(t, err) - assert.Equal(t, "user-123", state.DistinctID) -} - -func TestServiceCapturePrefersInMemoryUserIDOverPersistedDistinctID(t *testing.T) { - now := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC) - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - analytics := &fakeAnalytics{enabled: true} - require.NoError(t, SaveState(State{ - Enabled: true, - DeviceID: uuid.NewString(), - SessionID: uuid.NewString(), - SessionLastActive: now, - SchemaVersion: SchemaVersion, - DistinctID: "old-user", - }, fsys)) - - service, err := NewService(fsys, Options{ - Analytics: analytics, - Now: func() time.Time { return now }, - IsCI: true, - }) - require.NoError(t, err) - - require.NoError(t, service.StitchLogin("new-user")) - require.NoError(t, service.Capture(context.Background(), EventLoginCompleted, nil, nil)) - - require.Len(t, analytics.captures, 1) - assert.Equal(t, "new-user", analytics.captures[0].distinctID) - - state, err := LoadState(fsys) - require.NoError(t, err) - assert.Equal(t, "old-user", state.DistinctID) -} - -func TestServiceClearDistinctIDFallsBackToDeviceID(t *testing.T) { - now := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC) - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - analytics := &fakeAnalytics{enabled: true} - - service, err := NewService(fsys, Options{ - Analytics: analytics, - Now: func() time.Time { return now }, - }) - require.NoError(t, err) - deviceID := service.state.DeviceID - require.NoError(t, service.StitchLogin("user-123")) - - require.NoError(t, service.ClearDistinctID()) - require.NoError(t, service.Capture(context.Background(), EventLoginCompleted, nil, nil)) - - require.Len(t, analytics.captures, 1) - assert.Equal(t, deviceID, analytics.captures[0].distinctID) - - state, err := LoadState(fsys) - require.NoError(t, err) - assert.Empty(t, state.DistinctID) -} - -func TestServiceCaptureIncludesLinkedProjectGroups(t *testing.T) { - now := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC) - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - analytics := &fakeAnalytics{enabled: true} - require.NoError(t, SaveLinkedProject(api.V1ProjectWithDatabaseResponseOutput{ - Ref: "proj_123", - Name: "My Project", - OrganizationId: "org_123", - OrganizationSlug: "acme", - }, fsys)) - - service, err := NewService(fsys, Options{ - Analytics: analytics, - Now: func() time.Time { return now }, - }) - require.NoError(t, err) - - require.NoError(t, service.Capture(context.Background(), EventStackStarted, nil, nil)) - - require.Len(t, analytics.captures, 1) - assert.Equal(t, map[string]string{ - GroupOrganization: "org_123", - GroupProject: "proj_123", - }, analytics.captures[0].groups) -} - -func TestServiceNeedsIdentityStitch(t *testing.T) { - now := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC) - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - analytics := &fakeAnalytics{enabled: true} - - service, err := NewService(fsys, Options{ - Analytics: analytics, - Now: func() time.Time { return now }, - IsTTY: true, - }) - require.NoError(t, err) - - t.Run("true when DistinctID is empty", func(t *testing.T) { - assert.True(t, service.NeedsIdentityStitch()) - }) - - t.Run("false after StitchLogin", func(t *testing.T) { - require.NoError(t, service.StitchLogin("user-123")) - assert.False(t, service.NeedsIdentityStitch()) - }) - - t.Run("true in CI with empty DistinctID so capture stamping can start", func(t *testing.T) { - ciFsys := afero.NewMemMapFs() - ciService, err := NewService(ciFsys, Options{ - Analytics: &fakeAnalytics{enabled: true}, - Now: func() time.Time { return now }, - IsCI: true, - }) - require.NoError(t, err) - assert.True(t, ciService.NeedsIdentityStitch()) - }) - - t.Run("false after StitchLogin in ephemeral runtime despite nothing persisted", func(t *testing.T) { - ephemeralFsys := afero.NewMemMapFs() - ephemeralService, err := NewService(ephemeralFsys, Options{ - Analytics: &fakeAnalytics{enabled: true}, - Now: func() time.Time { return now }, - IsCI: true, - }) - require.NoError(t, err) - require.NoError(t, ephemeralService.StitchLogin("user-123")) - assert.False(t, ephemeralService.NeedsIdentityStitch()) - }) - - t.Run("true in first-run non-TTY runtime", func(t *testing.T) { - ephemeralFsys := afero.NewMemMapFs() - ephemeralService, err := NewService(ephemeralFsys, Options{ - Analytics: &fakeAnalytics{enabled: true}, - Now: func() time.Time { return now }, - }) - require.NoError(t, err) - assert.True(t, ephemeralService.NeedsIdentityStitch()) - }) - - t.Run("true in persisted non-TTY runtime", func(t *testing.T) { - persistedFsys := afero.NewMemMapFs() - require.NoError(t, SaveState(State{ - Enabled: true, - DeviceID: uuid.NewString(), - SessionID: uuid.NewString(), - SessionLastActive: now, - SchemaVersion: SchemaVersion, - }, persistedFsys)) - - persistedService, err := NewService(persistedFsys, Options{ - Analytics: &fakeAnalytics{enabled: true}, - Now: func() time.Time { return now }, - }) - require.NoError(t, err) - assert.True(t, persistedService.NeedsIdentityStitch()) - }) -} - -func TestServiceCaptureHonorsConsentAndEnvOptOut(t *testing.T) { - now := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC) - - t.Run("disabled telemetry file suppresses capture", func(t *testing.T) { - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - analytics := &fakeAnalytics{enabled: true} - require.NoError(t, SaveState(State{ - Enabled: false, - DeviceID: uuid.NewString(), - SessionID: uuid.NewString(), - SessionLastActive: now, - SchemaVersion: SchemaVersion, - }, fsys)) - - service, err := NewService(fsys, Options{ - Analytics: analytics, - Now: func() time.Time { return now }, - }) - require.NoError(t, err) - - require.NoError(t, service.Capture(context.Background(), EventCommandExecuted, nil, nil)) - assert.Empty(t, analytics.captures) - }) - - t.Run("DO_NOT_TRACK suppresses capture", func(t *testing.T) { - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - t.Setenv("DO_NOT_TRACK", "1") - fsys := afero.NewMemMapFs() - analytics := &fakeAnalytics{enabled: true} - - service, err := NewService(fsys, Options{ - Analytics: analytics, - Now: func() time.Time { return now }, - }) - require.NoError(t, err) - - require.NoError(t, service.Capture(context.Background(), EventCommandExecuted, nil, nil)) - assert.Empty(t, analytics.captures) - }) -} diff --git a/apps/cli-go/internal/telemetry/state.go b/apps/cli-go/internal/telemetry/state.go deleted file mode 100644 index 3808abb78a..0000000000 --- a/apps/cli-go/internal/telemetry/state.go +++ /dev/null @@ -1,192 +0,0 @@ -package telemetry - -import ( - "encoding/json" - "os" - "path/filepath" - "time" - - "github.com/go-errors/errors" - "github.com/google/uuid" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/utils" -) - -const SchemaVersion = 1 - -const sessionRotationThreshold = 30 * time.Minute - -// errMalformedState marks any read where the file existed but couldn't be -// decoded into a State — covers JSON syntax errors, unexpected types -// (e.g. session_last_active stored as a number), and field-level unmarshal -// failures from time.Time / uuid. Used to trigger fresh-state creation. -var errMalformedState = errors.New("malformed telemetry state") - -type State struct { - Enabled bool `json:"enabled"` - DeviceID string `json:"device_id"` - SessionID string `json:"session_id"` - SessionLastActive time.Time `json:"session_last_active"` - DistinctID string `json:"distinct_id,omitempty"` - SchemaVersion int `json:"schema_version"` -} - -type rawState struct { - Enabled *bool `json:"enabled"` - Consent *string `json:"consent"` - DeviceID string `json:"device_id"` - SessionID string `json:"session_id"` - SessionLastActive json.RawMessage `json:"session_last_active"` - DistinctID string `json:"distinct_id,omitempty"` - SchemaVersion int `json:"schema_version"` -} - -func telemetryPath() (string, error) { - home, err := utils.SupabaseHomeDir() - if err != nil { - return "", err - } - return filepath.Join(home, "telemetry.json"), nil -} - -func parseConsent(raw rawState) (bool, bool, error) { - if raw.Consent != nil { - switch *raw.Consent { - case "granted": - return true, true, nil - case "denied": - return false, true, nil - default: - return false, false, errors.Errorf("%w: invalid consent", errMalformedState) - } - } - if raw.Enabled == nil { - return false, false, errors.Errorf("%w: missing enabled", errMalformedState) - } - return *raw.Enabled, false, nil -} - -func parseSessionLastActive(raw json.RawMessage, allowUnixMillis bool) (time.Time, error) { - var text string - if err := json.Unmarshal(raw, &text); err == nil { - parsed, err := time.Parse(time.RFC3339Nano, text) - if err != nil { - return time.Time{}, errors.Errorf("%w: invalid session_last_active", errMalformedState) - } - return parsed, nil - } - if allowUnixMillis { - var millis int64 - if err := json.Unmarshal(raw, &millis); err == nil { - return time.UnixMilli(millis).UTC(), nil - } - } - return time.Time{}, errors.Errorf("%w: invalid session_last_active", errMalformedState) -} - -func decodeState(contents []byte) (State, error) { - var raw rawState - if err := json.Unmarshal(contents, &raw); err != nil { - return State{}, errors.Errorf("%w: %v", errMalformedState, err) - } - enabled, allowUnixMillis, err := parseConsent(raw) - if err != nil { - return State{}, err - } - sessionLastActive, err := parseSessionLastActive(raw.SessionLastActive, allowUnixMillis) - if err != nil { - return State{}, err - } - if raw.DeviceID == "" || raw.SessionID == "" { - return State{}, errors.Errorf("%w: missing identity", errMalformedState) - } - schemaVersion := raw.SchemaVersion - if schemaVersion == 0 { - schemaVersion = SchemaVersion - } - return State{ - Enabled: enabled, - DeviceID: raw.DeviceID, - SessionID: raw.SessionID, - SessionLastActive: sessionLastActive, - DistinctID: raw.DistinctID, - SchemaVersion: schemaVersion, - }, nil -} - -func LoadState(fsys afero.Fs) (State, error) { - path, err := telemetryPath() - if err != nil { - return State{}, err - } - contents, err := afero.ReadFile(fsys, path) - if err != nil { - return State{}, err - } - return decodeState(contents) -} - -func SaveState(state State, fsys afero.Fs) error { - path, err := telemetryPath() - if err != nil { - return err - } - contents, err := json.Marshal(state) - if err != nil { - return errors.Errorf("failed to encode telemetry file: %w", err) - } - return utils.WriteFile(path, contents, fsys) -} - -func LoadOrCreateState(fsys afero.Fs, now time.Time) (State, bool, error) { - state, err := LoadState(fsys) - if err == nil { - if now.UTC().Sub(state.SessionLastActive) > sessionRotationThreshold { - state.SessionID = uuid.NewString() - } - state.SessionLastActive = now.UTC() - return state, false, SaveState(state, fsys) - } - // Treat a missing file OR an unparseable file as "no existing state" and - // recreate. Identity fields (device_id, session_id) are not worth - // surfacing an error for — losing them is harmless. We only propagate - // genuine I/O errors (permissions, disk full) so the user can act. - if !errors.Is(err, os.ErrNotExist) && !errors.Is(err, errMalformedState) { - return State{}, false, err - } - state = State{ - Enabled: true, - DeviceID: uuid.NewString(), - SessionID: uuid.NewString(), - SessionLastActive: now.UTC(), - SchemaVersion: SchemaVersion, - } - return state, true, SaveState(state, fsys) -} - -func Disabled(fsys afero.Fs, now time.Time) (bool, error) { - if os.Getenv("DO_NOT_TRACK") == "1" { - return true, nil - } - if os.Getenv("SUPABASE_TELEMETRY_DISABLED") == "1" { - return true, nil - } - state, _, err := LoadOrCreateState(fsys, now) - if err != nil { - return false, err - } - return !state.Enabled, nil -} - -func SetEnabled(fsys afero.Fs, enabled bool, now time.Time) (State, error) { - state, _, err := LoadOrCreateState(fsys, now) - if err != nil { - return State{}, err - } - state.Enabled = enabled - return state, SaveState(state, fsys) -} - -func Status(fsys afero.Fs, now time.Time) (State, bool, error) { - return LoadOrCreateState(fsys, now) -} diff --git a/apps/cli-go/internal/telemetry/state_test.go b/apps/cli-go/internal/telemetry/state_test.go deleted file mode 100644 index 7ca3a178c6..0000000000 --- a/apps/cli-go/internal/telemetry/state_test.go +++ /dev/null @@ -1,289 +0,0 @@ -package telemetry - -import ( - "testing" - "time" - - "github.com/google/uuid" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestTelemetryPath(t *testing.T) { - t.Run("uses SUPABASE_HOME when set", func(t *testing.T) { - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - t.Setenv("HOME", "/tmp/ignored-home") - - path, err := telemetryPath() - - require.NoError(t, err) - assert.Equal(t, "/tmp/supabase-home/telemetry.json", path) - }) - - t.Run("falls back to HOME/.supabase", func(t *testing.T) { - t.Setenv("SUPABASE_HOME", "") - t.Setenv("HOME", "/tmp/home") - - path, err := telemetryPath() - - require.NoError(t, err) - assert.Equal(t, "/tmp/home/.supabase/telemetry.json", path) - }) -} - -func TestLoadOrCreateState(t *testing.T) { - now := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC) - - t.Run("creates default state and writes it", func(t *testing.T) { - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - - state, created, err := LoadOrCreateState(fsys, now) - - require.NoError(t, err) - assert.True(t, created) - assert.True(t, state.Enabled) - assert.Equal(t, SchemaVersion, state.SchemaVersion) - assert.Equal(t, now, state.SessionLastActive) - assert.Empty(t, state.DistinctID) - assert.NoError(t, uuid.Validate(state.DeviceID)) - assert.NoError(t, uuid.Validate(state.SessionID)) - - saved, err := LoadState(fsys) - require.NoError(t, err) - assert.Equal(t, state, saved) - }) - - t.Run("updates last active and preserves existing state", func(t *testing.T) { - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - initial := State{ - Enabled: false, - DeviceID: uuid.NewString(), - SessionID: uuid.NewString(), - SessionLastActive: now.Add(-10 * time.Minute), - DistinctID: "user-123", - SchemaVersion: SchemaVersion, - } - require.NoError(t, SaveState(initial, fsys)) - - state, created, err := LoadOrCreateState(fsys, now) - - require.NoError(t, err) - assert.False(t, created) - assert.False(t, state.Enabled) - assert.Equal(t, initial.DeviceID, state.DeviceID) - assert.Equal(t, initial.SessionID, state.SessionID) - assert.Equal(t, "user-123", state.DistinctID) - assert.Equal(t, now, state.SessionLastActive) - }) - - t.Run("reads disabled TypeScript telemetry config", func(t *testing.T) { - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - path, err := telemetryPath() - require.NoError(t, err) - require.NoError(t, fsys.MkdirAll("/tmp/supabase-home", 0755)) - require.NoError(t, afero.WriteFile( - fsys, - path, - []byte(`{"consent":"denied","device_id":"ts-device","session_id":"ts-session","session_last_active":1776770348993,"distinct_id":"user-123"}`), - 0644, - )) - - state, created, err := LoadOrCreateState(fsys, now) - - require.NoError(t, err) - assert.False(t, created) - assert.False(t, state.Enabled) - assert.Equal(t, "ts-device", state.DeviceID) - assert.Equal(t, "ts-session", state.SessionID) - assert.Equal(t, "user-123", state.DistinctID) - assert.Equal(t, SchemaVersion, state.SchemaVersion) - }) - - t.Run("reads enabled TypeScript telemetry config", func(t *testing.T) { - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - path, err := telemetryPath() - require.NoError(t, err) - require.NoError(t, fsys.MkdirAll("/tmp/supabase-home", 0755)) - require.NoError(t, afero.WriteFile( - fsys, - path, - []byte(`{"consent":"granted","device_id":"ts-device","session_id":"ts-session","session_last_active":1776770348993,"distinct_id":"user-123"}`), - 0644, - )) - - state, created, err := LoadOrCreateState(fsys, now) - - require.NoError(t, err) - assert.False(t, created) - assert.True(t, state.Enabled) - assert.Equal(t, "ts-device", state.DeviceID) - assert.Equal(t, "ts-session", state.SessionID) - assert.Equal(t, "user-123", state.DistinctID) - assert.Equal(t, SchemaVersion, state.SchemaVersion) - }) - - t.Run("recovers from corrupted state file", func(t *testing.T) { - // Each entry simulates a real-world corruption shape we've observed. - corruptions := map[string][]byte{ - "empty file": {}, - "truncated json": []byte(`{"enabled":tru`), - "session_last_active is a number (not a string)": []byte(`{"enabled":true,"device_id":"d","session_id":"s","session_last_active":1776770348993,"schema_version":1}`), - "session_last_active is a malformed string": []byte(`{"enabled":true,"device_id":"d","session_id":"s","session_last_active":"not-a-time","schema_version":1}`), - } - for label, contents := range corruptions { - t.Run(label, func(t *testing.T) { - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - path, err := telemetryPath() - require.NoError(t, err) - require.NoError(t, fsys.MkdirAll("/tmp/supabase-home", 0755)) - require.NoError(t, afero.WriteFile(fsys, path, contents, 0644)) - - state, created, err := LoadOrCreateState(fsys, now) - - require.NoError(t, err) - assert.True(t, created) - assert.True(t, state.Enabled) - assert.Equal(t, SchemaVersion, state.SchemaVersion) - assert.NoError(t, uuid.Validate(state.DeviceID)) - assert.NoError(t, uuid.Validate(state.SessionID)) - saved, err := LoadState(fsys) - require.NoError(t, err) - assert.Equal(t, state, saved) - }) - } - }) - - t.Run("rotates stale session after inactivity threshold", func(t *testing.T) { - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - initial := State{ - Enabled: true, - DeviceID: uuid.NewString(), - SessionID: uuid.NewString(), - SessionLastActive: now.Add(-(sessionRotationThreshold + time.Minute)), - DistinctID: "user-123", - SchemaVersion: SchemaVersion, - } - require.NoError(t, SaveState(initial, fsys)) - - state, created, err := LoadOrCreateState(fsys, now) - - require.NoError(t, err) - assert.False(t, created) - assert.Equal(t, initial.DeviceID, state.DeviceID) - assert.NotEqual(t, initial.SessionID, state.SessionID) - assert.Equal(t, "user-123", state.DistinctID) - assert.Equal(t, now, state.SessionLastActive) - - saved, err := LoadState(fsys) - require.NoError(t, err) - assert.Equal(t, state, saved) - }) -} - -func TestTelemetryDisabled(t *testing.T) { - now := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC) - - t.Run("honors DO_NOT_TRACK", func(t *testing.T) { - t.Setenv("DO_NOT_TRACK", "1") - fsys := afero.NewMemMapFs() - - disabled, err := Disabled(fsys, now) - - require.NoError(t, err) - assert.True(t, disabled) - }) - - t.Run("honors SUPABASE_TELEMETRY_DISABLED", func(t *testing.T) { - t.Setenv("SUPABASE_TELEMETRY_DISABLED", "1") - fsys := afero.NewMemMapFs() - - disabled, err := Disabled(fsys, now) - - require.NoError(t, err) - assert.True(t, disabled) - }) - - t.Run("honors disabled state file", func(t *testing.T) { - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - require.NoError(t, SaveState(State{ - Enabled: false, - DeviceID: uuid.NewString(), - SessionID: uuid.NewString(), - SessionLastActive: now, - SchemaVersion: SchemaVersion, - }, fsys)) - - disabled, err := Disabled(fsys, now) - - require.NoError(t, err) - assert.True(t, disabled) - }) - - t.Run("creates enabled state when missing", func(t *testing.T) { - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - - disabled, err := Disabled(fsys, now) - - require.NoError(t, err) - assert.False(t, disabled) - }) -} - -func TestSetEnabledAndStatus(t *testing.T) { - now := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC) - - t.Run("disable preserves identity fields", func(t *testing.T) { - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - initial, _, err := LoadOrCreateState(fsys, now) - require.NoError(t, err) - initial.DistinctID = "user-123" - require.NoError(t, SaveState(initial, fsys)) - - state, err := SetEnabled(fsys, false, now.Add(time.Minute)) - - require.NoError(t, err) - assert.False(t, state.Enabled) - assert.Equal(t, initial.DeviceID, state.DeviceID) - assert.Equal(t, initial.SessionID, state.SessionID) - assert.Equal(t, "user-123", state.DistinctID) - }) - - t.Run("enable flips disabled state back on", func(t *testing.T) { - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - require.NoError(t, SaveState(State{ - Enabled: false, - DeviceID: uuid.NewString(), - SessionID: uuid.NewString(), - SessionLastActive: now, - SchemaVersion: SchemaVersion, - }, fsys)) - - state, err := SetEnabled(fsys, true, now.Add(time.Minute)) - - require.NoError(t, err) - assert.True(t, state.Enabled) - }) - - t.Run("status creates default state when missing", func(t *testing.T) { - t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") - fsys := afero.NewMemMapFs() - - state, created, err := Status(fsys, now) - - require.NoError(t, err) - assert.True(t, created) - assert.True(t, state.Enabled) - assert.NoError(t, uuid.Validate(state.DeviceID)) - }) -} diff --git a/apps/cli-go/internal/testing/apitest/docker.go b/apps/cli-go/internal/testing/apitest/docker.go deleted file mode 100644 index b3c80f4b40..0000000000 --- a/apps/cli-go/internal/testing/apitest/docker.go +++ /dev/null @@ -1,156 +0,0 @@ -package apitest - -import ( - "bytes" - "fmt" - "io" - "net/http" - "strings" - - "github.com/docker/docker/api" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/versions" - "github.com/docker/docker/api/types/volume" - "github.com/docker/docker/client" - "github.com/docker/docker/pkg/stdcopy" - "github.com/go-errors/errors" - "github.com/h2non/gock" -) - -const mockHost = "http://127.0.0.1" - -func MockDocker(docker *client.Client) error { - // Skip setup if docker is already mocked - if docker.DaemonHost() == mockHost { - return nil - } - if err := client.WithVersion(api.DefaultVersion)(docker); err != nil { - return err - } - // Safe to ignore errors as transport will be replaced by gock - _ = client.WithHost(mockHost)(docker) - return client.WithHTTPClient(http.DefaultClient)(docker) -} - -// Ref: internal/utils/docker.go::DockerStart -func MockDockerStart(docker *client.Client, imageID, containerID string) { - gock.New(docker.DaemonHost()). - Get("/v" + docker.ClientVersion() + "/images/" + imageID + "/json"). - Reply(http.StatusOK). - JSON(image.InspectResponse{}) - gock.New(docker.DaemonHost()). - Post("/v" + docker.ClientVersion() + "/networks/create"). - Reply(http.StatusCreated). - JSON(network.CreateResponse{}) - gock.New(docker.DaemonHost()). - Post("/v" + docker.ClientVersion() + "/volumes/create"). - Persist(). - Reply(http.StatusCreated). - JSON(volume.Volume{}) - gock.New(docker.DaemonHost()). - Post("/v" + docker.ClientVersion() + "/containers/create"). - Reply(http.StatusOK). - JSON(container.CreateResponse{ID: containerID}) - gock.New(docker.DaemonHost()). - Post("/v" + docker.ClientVersion() + "/containers/" + containerID + "/start"). - Reply(http.StatusAccepted) -} - -// Ref: internal/utils/docker.go::DockerRemoveAll -func MockDockerStop(docker *client.Client) { - gock.New(docker.DaemonHost()). - Get("/v" + docker.ClientVersion() + "/containers/json"). - Reply(http.StatusOK). - JSON([]container.Summary{}) - gock.New(docker.DaemonHost()). - Post("/v" + docker.ClientVersion() + "/containers/prune"). - Reply(http.StatusOK). - JSON(container.PruneReport{}) - if !versions.GreaterThanOrEqualTo(docker.ClientVersion(), "1.42") { - gock.New(docker.DaemonHost()). - Post("/v"+docker.ClientVersion()+"/volumes/prune"). - MatchParam("filters", `"all":{"true":true}`). - ReplyError(errors.New(`failed to parse filters for all=true&label=com.supabase.cli.project%3Dtest: "all" is an invalid volume filter`)) - } - gock.New(docker.DaemonHost()). - Post("/v" + docker.ClientVersion() + "/volumes/prune"). - Reply(http.StatusOK). - JSON(volume.PruneReport{}) - gock.New(docker.DaemonHost()). - Post("/v" + docker.ClientVersion() + "/networks/prune"). - Reply(http.StatusOK). - JSON(network.PruneReport{}) -} - -// Ref: internal/utils/docker.go::DockerRunOnce -func setupDockerLogs(docker *client.Client, containerID, stdout string, exitCode int) error { - err := MockDockerLogsStream(docker, containerID, exitCode, strings.NewReader(stdout)) - gock.New(docker.DaemonHost()). - Delete("/v" + docker.ClientVersion() + "/containers/" + containerID). - Reply(http.StatusOK) - return err -} - -func MockDockerLogsStream(docker *client.Client, containerID string, exitCode int, r io.Reader) error { - var body bytes.Buffer - writer := stdcopy.NewStdWriter(&body, stdcopy.Stdout) - _, err := io.Copy(writer, r) - gock.New(docker.DaemonHost()). - Get("/v"+docker.ClientVersion()+"/containers/"+containerID+"/logs"). - Reply(http.StatusOK). - SetHeader("Content-Type", "application/vnd.docker.raw-stream"). - Body(&body) - gock.New(docker.DaemonHost()). - Get("/v" + docker.ClientVersion() + "/containers/" + containerID + "/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{ - ExitCode: exitCode, - }}}) - return err -} - -func MockDockerLogs(docker *client.Client, containerID, stdout string) error { - return setupDockerLogs(docker, containerID, stdout, 0) -} - -func MockDockerLogsExitCode(docker *client.Client, containerID string, exitCode int) error { - return setupDockerLogs(docker, containerID, "", exitCode) -} - -// MockDockerErrorLogs streams stderr output alongside a non-zero exit code, -// mirroring a container whose command failed (e.g. pg_dump unable to reach the -// database). -func MockDockerErrorLogs(docker *client.Client, containerID string, exitCode int, stderr string) error { - var body bytes.Buffer - writer := stdcopy.NewStdWriter(&body, stdcopy.Stderr) - if _, err := io.Copy(writer, strings.NewReader(stderr)); err != nil { - return err - } - gock.New(docker.DaemonHost()). - Get("/v"+docker.ClientVersion()+"/containers/"+containerID+"/logs"). - Reply(http.StatusOK). - SetHeader("Content-Type", "application/vnd.docker.raw-stream"). - Body(&body) - gock.New(docker.DaemonHost()). - Get("/v" + docker.ClientVersion() + "/containers/" + containerID + "/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{ - ExitCode: exitCode, - }}}) - gock.New(docker.DaemonHost()). - Delete("/v" + docker.ClientVersion() + "/containers/" + containerID). - Reply(http.StatusOK) - return nil -} - -func ListUnmatchedRequests() []string { - result := make([]string, len(gock.GetUnmatchedRequests())) - for i, r := range gock.GetUnmatchedRequests() { - result[i] = fmt.Sprintln(r.Method, r.URL.Path) - } - return result -} diff --git a/apps/cli-go/internal/testing/apitest/platform.go b/apps/cli-go/internal/testing/apitest/platform.go deleted file mode 100644 index 6ae3d50ded..0000000000 --- a/apps/cli-go/internal/testing/apitest/platform.go +++ /dev/null @@ -1,60 +0,0 @@ -package apitest - -import ( - "crypto/rand" - "encoding/hex" - "fmt" - "os" - "testing" - - "github.com/h2non/gock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func RandomAccessToken(t *testing.T) []byte { - data := make([]byte, 20) - _, err := rand.Read(data) - require.NoError(t, err) - token := make([]byte, 44) - copy(token, "sbp_") - hex.Encode(token[4:], data) - return token -} - -const letters = "abcdefghijklmnopqrstuvwxyz" - -func RandomProjectRef() string { - data := make([]byte, 20) - _, err := rand.Read(data) - if err != nil { - fmt.Fprintln(os.Stderr, err) - } - for i := range data { - n := int(data[i]) % len(letters) - data[i] = letters[n] - } - return string(data) -} - -func AssertRequestsDone(t *testing.T) { - assert.False(t, gock.HasUnmatchedRequest()) - for _, r := range gock.GetUnmatchedRequests() { - fmt.Fprintln(os.Stderr, "Unmatched:", r.Method, r.URL.Path) - } - assert.True(t, gock.IsDone()) - for _, p := range gock.Pending() { - fmt.Fprintln(os.Stderr, "Pending:", p.Request().Method, p.Request().URLStruct.Path) - } -} - -func MockPlatformAPI(t *testing.T) func() { - // Setup valid access token - token := RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) - teardown := func() { - AssertRequestsDone(t) - gock.OffAll() - } - return teardown -} diff --git a/apps/cli-go/internal/testing/fstest/create.go b/apps/cli-go/internal/testing/fstest/create.go deleted file mode 100644 index a8c0734b8d..0000000000 --- a/apps/cli-go/internal/testing/fstest/create.go +++ /dev/null @@ -1,20 +0,0 @@ -package fstest - -import ( - "io/fs" - "strings" - - "github.com/spf13/afero" -) - -type CreateErrorFs struct { - afero.MemMapFs - DenyPath string -} - -func (m *CreateErrorFs) Create(name string) (afero.File, error) { - if strings.HasPrefix(name, m.DenyPath) { - return nil, fs.ErrPermission - } - return m.MemMapFs.Create(name) -} diff --git a/apps/cli-go/internal/testing/fstest/open.go b/apps/cli-go/internal/testing/fstest/open.go deleted file mode 100644 index bc756e3cb8..0000000000 --- a/apps/cli-go/internal/testing/fstest/open.go +++ /dev/null @@ -1,28 +0,0 @@ -package fstest - -import ( - "io/fs" - "os" - "strings" - - "github.com/spf13/afero" -) - -type OpenErrorFs struct { - afero.MemMapFs - DenyPath string -} - -func (m *OpenErrorFs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) { - if strings.HasPrefix(name, m.DenyPath) { - return nil, fs.ErrPermission - } - return m.MemMapFs.OpenFile(name, flag, perm) -} - -func (m *OpenErrorFs) Open(name string) (afero.File, error) { - if strings.HasPrefix(name, m.DenyPath) { - return nil, fs.ErrPermission - } - return m.MemMapFs.Open(name) -} diff --git a/apps/cli-go/internal/testing/fstest/stat.go b/apps/cli-go/internal/testing/fstest/stat.go deleted file mode 100644 index 3b53f882d6..0000000000 --- a/apps/cli-go/internal/testing/fstest/stat.go +++ /dev/null @@ -1,20 +0,0 @@ -package fstest - -import ( - "io/fs" - "strings" - - "github.com/spf13/afero" -) - -type StatErrorFs struct { - afero.MemMapFs - DenyPath string -} - -func (m *StatErrorFs) Stat(name string) (fs.FileInfo, error) { - if strings.HasPrefix(name, m.DenyPath) { - return nil, fs.ErrPermission - } - return m.MemMapFs.Stat(name) -} diff --git a/apps/cli-go/internal/testing/fstest/stdin.go b/apps/cli-go/internal/testing/fstest/stdin.go deleted file mode 100644 index 63e1a8a259..0000000000 --- a/apps/cli-go/internal/testing/fstest/stdin.go +++ /dev/null @@ -1,28 +0,0 @@ -package fstest - -import ( - "os" - "strings" - "testing" - - "github.com/stretchr/testify/require" -) - -func MockStdin(t *testing.T, input string) func() { - // Setup stdin - r, w, err := os.Pipe() - require.NoError(t, err) - if len(input) > 0 && !strings.HasSuffix(input, "\n") { - input += "\n" - } - _, err = w.WriteString(input) - require.NoError(t, err) - require.NoError(t, w.Close()) - // Replace stdin - oldStdin := os.Stdin - teardown := func() { - os.Stdin = oldStdin - } - os.Stdin = r - return teardown -} diff --git a/apps/cli-go/internal/testing/fstest/stdout.go b/apps/cli-go/internal/testing/fstest/stdout.go deleted file mode 100644 index 06c7bb3dc8..0000000000 --- a/apps/cli-go/internal/testing/fstest/stdout.go +++ /dev/null @@ -1,26 +0,0 @@ -package fstest - -import ( - "io" - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func MockStdout(t *testing.T, output string) func() { - r, w, err := os.Pipe() - require.NoError(t, err) - // Replace stdout - oldStdout := os.Stdout - teardown := func() { - os.Stdout = oldStdout - assert.NoError(t, w.Close()) - data, err := io.ReadAll(r) - assert.NoError(t, err) - assert.Equal(t, output, string(data)) - } - os.Stdout = w - return teardown -} diff --git a/apps/cli-go/internal/testing/helper/history.go b/apps/cli-go/internal/testing/helper/history.go deleted file mode 100644 index 95c846b7ad..0000000000 --- a/apps/cli-go/internal/testing/helper/history.go +++ /dev/null @@ -1,28 +0,0 @@ -package helper - -import ( - "github.com/supabase/cli/pkg/migration" - "github.com/supabase/cli/pkg/pgtest" -) - -func MockMigrationHistory(conn *pgtest.MockConn) *pgtest.MockConn { - conn.Query(migration.SET_LOCK_TIMEOUT). - Query(migration.CREATE_VERSION_SCHEMA). - Reply("CREATE SCHEMA"). - Query(migration.CREATE_VERSION_TABLE). - Reply("CREATE TABLE"). - Query(migration.ADD_STATEMENTS_COLUMN). - Reply("ALTER TABLE"). - Query(migration.ADD_NAME_COLUMN). - Reply("ALTER TABLE") - return conn -} - -func MockSeedHistory(conn *pgtest.MockConn) *pgtest.MockConn { - conn.Query(migration.SET_LOCK_TIMEOUT). - Query(migration.CREATE_VERSION_SCHEMA). - Reply("CREATE SCHEMA"). - Query(migration.CREATE_SEED_TABLE). - Reply("CREATE TABLE") - return conn -} diff --git a/apps/cli-go/internal/testing/helper/privileges.go b/apps/cli-go/internal/testing/helper/privileges.go deleted file mode 100644 index 4dcca23f58..0000000000 --- a/apps/cli-go/internal/testing/helper/privileges.go +++ /dev/null @@ -1,19 +0,0 @@ -package helper - -import "github.com/supabase/cli/pkg/pgtest" - -// MockApiPrivilegesRevoke queues the per-statement responses for the default Data API privilege -// revoke that start.ApplyApiPrivileges runs when [api].auto_expose_new_tables is unset or false. -// -// The statements mirror start.RevokeDefaultDataApiPrivilegesSql. They are inlined here rather -// than imported from the start package to avoid an import cycle with that package's own -// internal (package start) tests. -func MockApiPrivilegesRevoke(conn *pgtest.MockConn) *pgtest.MockConn { - conn.Query("alter default privileges for role postgres in schema public\n revoke select, insert, update, delete on tables from anon, authenticated, service_role"). - Reply("ALTER DEFAULT PRIVILEGES"). - Query("alter default privileges for role postgres in schema public\n revoke usage, select on sequences from anon, authenticated, service_role"). - Reply("ALTER DEFAULT PRIVILEGES"). - Query("alter default privileges for role postgres in schema public\n revoke execute on functions from anon, authenticated, service_role"). - Reply("ALTER DEFAULT PRIVILEGES") - return conn -} diff --git a/apps/cli-go/internal/utils/access_token.go b/apps/cli-go/internal/utils/access_token.go deleted file mode 100644 index 79489b564e..0000000000 --- a/apps/cli-go/internal/utils/access_token.go +++ /dev/null @@ -1,138 +0,0 @@ -package utils - -import ( - "fmt" - "os" - "path/filepath" - "regexp" - - "github.com/go-errors/errors" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/utils/credentials" - "github.com/zalando/go-keyring" -) - -var ( - AccessTokenPattern = regexp.MustCompile(`^sbp_(oauth_|v0_)?[a-f0-9]{40}$`) - ErrInvalidToken = errors.New("Invalid access token format. Must be like `sbp_0102...1920`.") - ErrMissingToken = errors.Errorf("Access token not provided. Supply an access token by running %s or setting the SUPABASE_ACCESS_TOKEN environment variable.", Aqua("supabase login")) - ErrNotLoggedIn = errors.New("You were not logged in, nothing to do.") -) - -const AccessTokenKey = "access-token" - -func LoadAccessTokenFS(fsys afero.Fs) (string, error) { - accessToken, err := loadAccessToken(fsys) - if err != nil { - return "", err - } - if !AccessTokenPattern.MatchString(accessToken) { - return "", errors.New(ErrInvalidToken) - } - return accessToken, nil -} - -func loadAccessToken(fsys afero.Fs) (string, error) { - logger := GetDebugLogger() - // Env takes precedence - if accessToken := os.Getenv("SUPABASE_ACCESS_TOKEN"); accessToken != "" { - fmt.Fprintln(logger, "Using access token from env var...") - return accessToken, nil - } - // Load from current profile - if accessToken, err := credentials.StoreProvider.Get(CurrentProfile.Name); err == nil { - fmt.Fprintln(logger, "Using access token for profile:", CurrentProfile.Name) - return accessToken, nil - } - // Load from legacy key for backwards compatibility - if accessToken, err := credentials.StoreProvider.Get(AccessTokenKey); err == nil { - fmt.Fprintln(logger, "Using access token from credentials store...") - return accessToken, nil - } - // Fallback to token file - return fallbackLoadToken(fsys) -} - -func fallbackLoadToken(fsys afero.Fs) (string, error) { - path, err := getAccessTokenPath() - if err != nil { - return "", err - } - accessToken, err := afero.ReadFile(fsys, path) - if errors.Is(err, os.ErrNotExist) { - return "", errors.New(ErrMissingToken) - } else if err != nil { - return "", errors.Errorf("failed to read access token file: %w", err) - } - fmt.Fprintln(GetDebugLogger(), "Using access token from file:", path) - return string(accessToken), nil -} - -func SaveAccessToken(accessToken string, fsys afero.Fs) error { - // Validate access token - if !AccessTokenPattern.MatchString(accessToken) { - return errors.New(ErrInvalidToken) - } - // Save to current profile - if err := credentials.StoreProvider.Set(CurrentProfile.Name, accessToken); err != nil { - fmt.Fprintln(GetDebugLogger(), err) - } else { - return nil - } - // Fallback to token file - return fallbackSaveToken(accessToken, fsys) -} - -func fallbackSaveToken(accessToken string, fsys afero.Fs) error { - path, err := getAccessTokenPath() - if err != nil { - return err - } - if err := MkdirIfNotExistFS(fsys, filepath.Dir(path)); err != nil { - return err - } - if err := afero.WriteFile(fsys, path, []byte(accessToken), 0600); err != nil { - return errors.Errorf("failed to save access token file: %w", err) - } - return nil -} - -func DeleteAccessToken(fsys afero.Fs) error { - // Always delete the fallback token file to handle legacy CLI - if err := fallbackDeleteToken(fsys); err != nil && !errors.Is(err, os.ErrNotExist) { - return err - } - // Always delete from legacy keyring - if err := credentials.StoreProvider.Delete(AccessTokenKey); err != nil && !errors.Is(err, keyring.ErrNotFound) { - fmt.Fprintln(GetDebugLogger(), err) - } - // Delete profile from native credentials store - if err := credentials.StoreProvider.Delete(CurrentProfile.Name); err != nil { - if errors.Is(err, credentials.ErrNotSupported) || - errors.Is(err, keyring.ErrUnsupportedPlatform) || - errors.Is(err, keyring.ErrNotFound) { - return errors.New(ErrNotLoggedIn) - } - return errors.Errorf("failed to delete access token from keyring: %w", err) - } - return nil -} - -func fallbackDeleteToken(fsys afero.Fs) error { - path, err := getAccessTokenPath() - if err != nil { - return err - } - if err := fsys.Remove(path); err != nil { - return errors.Errorf("failed to remove access token file: %w", err) - } - return nil -} - -func getAccessTokenPath() (string, error) { - home, err := SupabaseHomeDir() - if err != nil { - return "", err - } - return filepath.Join(home, AccessTokenKey), nil -} diff --git a/apps/cli-go/internal/utils/access_token_test.go b/apps/cli-go/internal/utils/access_token_test.go deleted file mode 100644 index 602dec1f6a..0000000000 --- a/apps/cli-go/internal/utils/access_token_test.go +++ /dev/null @@ -1,243 +0,0 @@ -package utils - -import ( - "os" - "path/filepath" - "testing" - - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/testing/apitest" - "github.com/supabase/cli/internal/testing/fstest" - "github.com/supabase/cli/internal/utils/credentials" - "github.com/zalando/go-keyring" -) - -func TestLoadToken(t *testing.T) { - keyring.MockInit() - token := string(apitest.RandomAccessToken(t)) - - t.Run("loads token from env var", func(t *testing.T) { - t.Setenv("SUPABASE_ACCESS_TOKEN", token) - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Run test - loaded, err := LoadAccessTokenFS(fsys) - // Check error - assert.NoError(t, err) - assert.Equal(t, token, loaded) - }) - - t.Run("loads v0 token from env var", func(t *testing.T) { - v0Token := "sbp_v0_" + token[len("sbp_"):] - t.Setenv("SUPABASE_ACCESS_TOKEN", v0Token) - fsys := afero.NewMemMapFs() - loaded, err := LoadAccessTokenFS(fsys) - assert.NoError(t, err) - assert.Equal(t, v0Token, loaded) - }) - - t.Run("throws error on unknown version prefix", func(t *testing.T) { - t.Setenv("SUPABASE_ACCESS_TOKEN", "sbp_v1_"+token[len("sbp_"):]) - fsys := afero.NewMemMapFs() - loaded, err := LoadAccessTokenFS(fsys) - assert.ErrorIs(t, err, ErrInvalidToken) - assert.Empty(t, loaded) - }) - - t.Run("throws error on invalid token", func(t *testing.T) { - t.Setenv("SUPABASE_ACCESS_TOKEN", "invalid") - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Run test - loaded, err := LoadAccessTokenFS(fsys) - // Check error - assert.ErrorIs(t, err, ErrInvalidToken) - assert.Empty(t, loaded) - }) - - t.Run("throws error on missing token", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Run test - loaded, err := LoadAccessTokenFS(fsys) - // Check error - assert.ErrorIs(t, err, ErrMissingToken) - assert.Empty(t, loaded) - }) -} - -func TestLoadTokenFallback(t *testing.T) { - t.Run("fallback loads from file", func(t *testing.T) { - path, err := getAccessTokenPath() - assert.NoError(t, err) - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, afero.WriteFile(fsys, path, []byte{}, 0600)) - // Run test - token, err := fallbackLoadToken(fsys) - // Check error - assert.NoError(t, err) - assert.Empty(t, token) - }) - - t.Run("throws error on home dir failure", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewReadOnlyFs(afero.NewMemMapFs()) - // Setup empty home directory - t.Setenv("HOME", "") - // Run test - token, err := fallbackLoadToken(fsys) - // Check error - assert.ErrorContains(t, err, "$HOME is not defined") - assert.Empty(t, token) - }) - - t.Run("throws error on read failure", func(t *testing.T) { - path, err := getAccessTokenPath() - assert.NoError(t, err) - // Setup in-memory fs - fsys := &fstest.OpenErrorFs{DenyPath: path} - // Run test - token, err := fallbackLoadToken(fsys) - // Check error - assert.ErrorContains(t, err, "permission denied") - assert.Empty(t, token) - }) -} - -func TestSaveToken(t *testing.T) { - keyring.MockInit() - token := string(apitest.RandomAccessToken(t)) - - t.Run("saves token to keyring", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Run test - assert.NoError(t, SaveAccessToken(token, fsys)) - // Validate saved token - saved, err := LoadAccessTokenFS(fsys) - assert.NoError(t, err) - assert.Equal(t, token, saved) - }) - - t.Run("throws error on invalid token", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Run test - err := SaveAccessToken("invalid", fsys) - // Check error - assert.ErrorIs(t, err, ErrInvalidToken) - }) -} - -func TestSaveTokenFallback(t *testing.T) { - token := string(apitest.RandomAccessToken(t)) - - t.Run("fallback saves to file", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Run test - assert.NoError(t, fallbackSaveToken(token, fsys)) - // Validate saved token - path, err := getAccessTokenPath() - assert.NoError(t, err) - contents, err := afero.ReadFile(fsys, path) - assert.NoError(t, err) - assert.Equal(t, []byte(token), contents) - }) - - t.Run("fallback saves to SUPABASE_HOME when configured", func(t *testing.T) { - t.Setenv("HOME", "/home/test") - t.Setenv("SUPABASE_HOME", "/custom/supabase") - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Run test - assert.NoError(t, fallbackSaveToken(token, fsys)) - // Validate saved token - configuredPath := filepath.Join("/custom/supabase", AccessTokenKey) - contents, err := afero.ReadFile(fsys, configuredPath) - assert.NoError(t, err) - assert.Equal(t, []byte(token), contents) - defaultPath := filepath.Join("/home/test", ".supabase", AccessTokenKey) - exists, err := afero.Exists(fsys, defaultPath) - assert.NoError(t, err) - assert.False(t, exists) - }) - - t.Run("throws error on home dir failure", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewReadOnlyFs(afero.NewMemMapFs()) - // Setup empty home directory - t.Setenv("HOME", "") - // Run test - err := fallbackSaveToken(token, fsys) - // Check error - assert.ErrorContains(t, err, "$HOME is not defined") - }) - - t.Run("throws error on permission denied", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewReadOnlyFs(afero.NewMemMapFs()) - // Run test - err := fallbackSaveToken(token, fsys) - // Check error - assert.ErrorContains(t, err, "operation not permitted") - }) - - t.Run("throws error on write failure", func(t *testing.T) { - home, err := os.UserHomeDir() - assert.NoError(t, err) - // Setup in-memory fs - fsys := &fstest.OpenErrorFs{DenyPath: home} - // Run test - err = fallbackSaveToken(token, fsys) - // Check error - assert.ErrorContains(t, err, "permission denied") - }) -} - -func TestDeleteToken(t *testing.T) { - t.Run("deletes both keyring and fallback", func(t *testing.T) { - token := string(apitest.RandomAccessToken(t)) - require.NoError(t, credentials.StoreProvider.Set(AccessTokenKey, token)) - require.NoError(t, credentials.StoreProvider.Set(CurrentProfile.Name, token)) - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, fallbackSaveToken(token, fsys)) - // Run test - err := DeleteAccessToken(fsys) - // Check error - assert.NoError(t, err) - _, err = credentials.StoreProvider.Get(CurrentProfile.Name) - assert.ErrorIs(t, err, keyring.ErrNotFound) - _, err = credentials.StoreProvider.Get(AccessTokenKey) - assert.ErrorIs(t, err, keyring.ErrNotFound) - path, err := getAccessTokenPath() - assert.NoError(t, err) - exists, err := afero.Exists(fsys, path) - assert.NoError(t, err) - assert.False(t, exists) - }) - - t.Run("throws error if not logged in", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Run test - err := DeleteAccessToken(fsys) - // Check error - assert.ErrorIs(t, err, ErrNotLoggedIn) - }) - - t.Run("throws error on home dir failure", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewReadOnlyFs(afero.NewMemMapFs()) - // Setup empty home directory - t.Setenv("HOME", "") - // Run test - err := DeleteAccessToken(fsys) - // Check error - assert.ErrorContains(t, err, "$HOME is not defined") - }) -} diff --git a/apps/cli-go/internal/utils/agent.go b/apps/cli-go/internal/utils/agent.go deleted file mode 100644 index f41ad85cb2..0000000000 --- a/apps/cli-go/internal/utils/agent.go +++ /dev/null @@ -1,24 +0,0 @@ -package utils - -import "github.com/supabase/cli/internal/utils/agent" - -// AgentMode is a global flag for overriding agent detection. -// Allowed values: "auto" (default), "yes", "no". -var AgentMode = EnumFlag{ - Allowed: []string{"auto", "yes", "no"}, - Value: "auto", -} - -// IsAgentMode returns true if the CLI is being used by an AI agent. -// "yes" forces agent mode on, "no" forces it off, and "auto" (default) -// auto-detects based on environment variables. -func IsAgentMode() bool { - switch AgentMode.Value { - case "yes": - return true - case "no": - return false - default: - return agent.IsAgent() - } -} diff --git a/apps/cli-go/internal/utils/agent/agent.go b/apps/cli-go/internal/utils/agent/agent.go deleted file mode 100644 index e3b0d7f88f..0000000000 --- a/apps/cli-go/internal/utils/agent/agent.go +++ /dev/null @@ -1,61 +0,0 @@ -package agent - -import ( - "os" - "strings" -) - -// IsClaudeCode reports whether the CLI is running inside Claude Code. -func IsClaudeCode() bool { - return os.Getenv("CLAUDECODE") != "" || os.Getenv("CLAUDE_CODE") != "" -} - -// IsAgent checks environment variables to detect if the CLI is being invoked -// by an AI coding agent. Based on the detection logic from Vercel's -// @vercel/functions/ai package. -func IsAgent() bool { - if v := strings.TrimSpace(os.Getenv("AI_AGENT")); v != "" { - return true - } - // Cursor - if os.Getenv("CURSOR_AGENT") != "" || os.Getenv("CURSOR_EXTENSION_HOST_ROLE") != "" { - return true - } - // Gemini - if os.Getenv("GEMINI_CLI") != "" { - return true - } - // Codex - if os.Getenv("CODEX_SANDBOX") != "" || os.Getenv("CODEX_CI") != "" || os.Getenv("CODEX_THREAD_ID") != "" { - return true - } - // Antigravity - if os.Getenv("ANTIGRAVITY_AGENT") != "" { - return true - } - // Augment - if os.Getenv("AUGMENT_AGENT") != "" { - return true - } - // OpenCode - if os.Getenv("OPENCODE_CLIENT") != "" { - return true - } - // Claude Code - if IsClaudeCode() { - return true - } - // Replit - if os.Getenv("REPL_ID") != "" { - return true - } - // GitHub Copilot - if os.Getenv("COPILOT_MODEL") != "" || os.Getenv("COPILOT_ALLOW_ALL") != "" || os.Getenv("COPILOT_GITHUB_TOKEN") != "" { - return true - } - // Devin - if _, err := os.Stat("/opt/.devin"); err == nil { - return true - } - return false -} diff --git a/apps/cli-go/internal/utils/agent/agent_test.go b/apps/cli-go/internal/utils/agent/agent_test.go deleted file mode 100644 index 1fc1c8be9b..0000000000 --- a/apps/cli-go/internal/utils/agent/agent_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package agent - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -// clearAgentEnv unsets all known agent environment variables for a clean test. -func clearAgentEnv(t *testing.T) { - t.Helper() - for _, key := range []string{ - "AI_AGENT", - "CURSOR_AGENT", - "CURSOR_EXTENSION_HOST_ROLE", - "GEMINI_CLI", - "CODEX_SANDBOX", "CODEX_CI", "CODEX_THREAD_ID", - "ANTIGRAVITY_AGENT", - "AUGMENT_AGENT", - "OPENCODE_CLIENT", - "CLAUDECODE", "CLAUDE_CODE", - "REPL_ID", - "COPILOT_MODEL", "COPILOT_ALLOW_ALL", "COPILOT_GITHUB_TOKEN", - } { - t.Setenv(key, "") - } -} - -func TestIsAgent(t *testing.T) { - t.Run("returns false with no agent env vars", func(t *testing.T) { - clearAgentEnv(t) - assert.False(t, IsAgent()) - }) - - t.Run("detects AI_AGENT", func(t *testing.T) { - clearAgentEnv(t) - t.Setenv("AI_AGENT", "custom-agent") - assert.True(t, IsAgent()) - }) - - t.Run("ignores empty AI_AGENT", func(t *testing.T) { - clearAgentEnv(t) - t.Setenv("AI_AGENT", " ") - assert.False(t, IsAgent()) - }) - - t.Run("detects Cursor via CURSOR_TRACE_ID", func(t *testing.T) { - t.Setenv("CURSOR_EXTENSION_HOST_ROLE", "agent-exec") - assert.True(t, IsAgent()) - }) - - t.Run("detects Cursor via CURSOR_AGENT", func(t *testing.T) { - clearAgentEnv(t) - t.Setenv("CURSOR_AGENT", "1") - assert.True(t, IsAgent()) - }) - - t.Run("detects Gemini via GEMINI_CLI", func(t *testing.T) { - t.Setenv("GEMINI_CLI", "1") - assert.True(t, IsAgent()) - }) - - t.Run("detects Codex via CODEX_SANDBOX", func(t *testing.T) { - t.Setenv("CODEX_SANDBOX", "1") - assert.True(t, IsAgent()) - }) - - t.Run("detects Claude Code via CLAUDECODE", func(t *testing.T) { - t.Setenv("CLAUDECODE", "1") - assert.True(t, IsAgent()) - }) - - t.Run("detects Claude Code via CLAUDE_CODE", func(t *testing.T) { - t.Setenv("CLAUDE_CODE", "1") - assert.True(t, IsAgent()) - }) - - t.Run("detects GitHub Copilot via COPILOT_MODEL", func(t *testing.T) { - t.Setenv("COPILOT_MODEL", "gpt-4") - assert.True(t, IsAgent()) - }) - - t.Run("detects Replit via REPL_ID", func(t *testing.T) { - t.Setenv("REPL_ID", "abc") - assert.True(t, IsAgent()) - }) - - t.Run("detects Augment via AUGMENT_AGENT", func(t *testing.T) { - t.Setenv("AUGMENT_AGENT", "1") - assert.True(t, IsAgent()) - }) - - t.Run("detects OpenCode via OPENCODE_CLIENT", func(t *testing.T) { - t.Setenv("OPENCODE_CLIENT", "1") - assert.True(t, IsAgent()) - }) - - t.Run("detects Antigravity via ANTIGRAVITY_AGENT", func(t *testing.T) { - t.Setenv("ANTIGRAVITY_AGENT", "1") - assert.True(t, IsAgent()) - }) -} diff --git a/apps/cli-go/internal/utils/api.go b/apps/cli-go/internal/utils/api.go deleted file mode 100644 index 06f37a8a50..0000000000 --- a/apps/cli-go/internal/utils/api.go +++ /dev/null @@ -1,165 +0,0 @@ -package utils - -import ( - "context" - "encoding/json" - "fmt" - "log" - "net" - "net/http" - "sync" - - "github.com/go-errors/errors" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/utils/cloudflare" - supabase "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/cast" -) - -const ( - DNS_GO_NATIVE = "native" - DNS_OVER_HTTPS = "https" -) - -var OnGotrueID func(string) - -var ( - clientOnce sync.Once - apiClient *supabase.ClientWithResponses - - DNSResolver = EnumFlag{ - Allowed: []string{DNS_GO_NATIVE, DNS_OVER_HTTPS}, - Value: DNS_GO_NATIVE, - } -) - -// Performs DNS lookup via HTTPS, in case firewall blocks native netgo resolver. -func FallbackLookupIP(ctx context.Context, host string) ([]string, error) { - if net.ParseIP(host) != nil { - return []string{host}, nil - } - // Ref: https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json - cf := cloudflare.NewCloudflareAPI() - data, err := cf.DNSQuery(ctx, cloudflare.DNSParams{Name: host}) - if err != nil { - return nil, err - } - // Look for first valid IP - var resolved []string - for _, answer := range data.Answer { - if answer.Type == cloudflare.TypeA || answer.Type == cloudflare.TypeAAAA { - resolved = append(resolved, answer.Data) - } - } - if len(resolved) == 0 { - return nil, errors.Errorf("failed to locate valid IP for %s; resolves to %#v", host, data.Answer) - } - return resolved, nil -} - -func ResolveCNAME(ctx context.Context, host string) (string, error) { - // Ref: https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json - cf := cloudflare.NewCloudflareAPI() - data, err := cf.DNSQuery(ctx, cloudflare.DNSParams{Name: host, Type: cast.Ptr(cloudflare.TypeCNAME)}) - if err != nil { - return "", err - } - // Look for first valid IP - for _, answer := range data.Answer { - if answer.Type == cloudflare.TypeCNAME { - return answer.Data, nil - } - } - serialized, err := json.MarshalIndent(data.Answer, "", " ") - if err != nil { - // we ignore the error (not great), and use the underlying struct in our error message - return "", errors.Errorf("failed to locate appropriate CNAME record for %s; resolves to %+v", host, data.Answer) - } - return "", errors.Errorf("failed to locate appropriate CNAME record for %s; resolves to %+v", host, serialized) -} - -type DialContextFunc func(context.Context, string, string) (net.Conn, error) - -// Wraps a DialContext with DNS-over-HTTPS as fallback resolver -func withFallbackDNS(dialContext DialContextFunc) DialContextFunc { - dnsOverHttps := func(ctx context.Context, network, address string) (net.Conn, error) { - host, port, err := net.SplitHostPort(address) - if err != nil { - return nil, errors.Errorf("failed to split host port: %w", err) - } - ip, err := FallbackLookupIP(ctx, host) - if err != nil { - return nil, err - } - conn, err := dialContext(ctx, network, net.JoinHostPort(ip[0], port)) - if err != nil { - return nil, errors.Errorf("failed to dial fallback: %w", err) - } - return conn, nil - } - if DNSResolver.Value == DNS_OVER_HTTPS { - return dnsOverHttps - } - nativeWithFallback := func(ctx context.Context, network, address string) (net.Conn, error) { - conn, err := dialContext(ctx, network, address) - // Workaround when pure Go DNS resolver fails https://github.com/golang/go/issues/12524 - if err, ok := err.(net.Error); ok && err.Timeout() { - if conn, err := dnsOverHttps(ctx, network, address); err == nil { - return conn, nil - } - } - if err != nil { - return nil, errors.Errorf("failed to dial native: %w", err) - } - return conn, nil - } - return nativeWithFallback -} - -func GetSupabase() *supabase.ClientWithResponses { - clientOnce.Do(func() { - token, err := LoadAccessTokenFS(afero.NewOsFs()) - if err != nil { - log.Fatalln(err) - } - if t, ok := http.DefaultTransport.(*http.Transport); ok { - t.DialContext = withFallbackDNS(t.DialContext) - } - transport := &identityTransport{ - RoundTripper: http.DefaultTransport, - onGotrueID: &OnGotrueID, - } - apiClient, err = supabase.NewClientWithResponses( - GetSupabaseAPIHost(), - supabase.WithHTTPClient(&http.Client{Transport: transport}), - supabase.WithRequestEditorFn(func(ctx context.Context, req *http.Request) error { - req.Header.Set("Authorization", "Bearer "+token) - req.Header.Set("User-Agent", "SupabaseCLI/"+Version) - return nil - }), - ) - if err != nil { - log.Fatalln(err) - } - }) - return apiClient -} - -// Used by unit tests -var DefaultApiHost = CurrentProfile.APIURL - -func GetSupabaseAPIHost() string { - return CurrentProfile.APIURL -} - -func GetSupabaseDashboardURL() string { - return CurrentProfile.DashboardURL -} - -func GetSupabaseHost(projectRef string) string { - return fmt.Sprintf("%s.%s", projectRef, CurrentProfile.ProjectHost) -} - -func GetSupabaseDbHost(projectRef string) string { - return "db." + GetSupabaseHost(projectRef) -} diff --git a/apps/cli-go/internal/utils/api_test.go b/apps/cli-go/internal/utils/api_test.go deleted file mode 100644 index 09365897d9..0000000000 --- a/apps/cli-go/internal/utils/api_test.go +++ /dev/null @@ -1,299 +0,0 @@ -package utils - -import ( - "context" - "errors" - "net" - "net/http" - "testing" - - "github.com/h2non/gock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/supabase/cli/internal/testing/apitest" - "github.com/supabase/cli/internal/utils/cloudflare" -) - -const host = "api.supabase.io" - -func TestLookupIP(t *testing.T) { - t.Run("resolves IPv4 with CloudFlare", func(t *testing.T) { - // Setup http mock - defer gock.OffAll() - gock.New("https://1.1.1.1"). - Get("/dns-query"). - MatchParam("name", host). - MatchHeader("accept", "application/dns-json"). - Reply(http.StatusOK). - JSON(&cloudflare.DNSResponse{Answer: []cloudflare.DNSAnswer{ - {Type: cloudflare.TypeA, Data: "127.0.0.1"}, - }}) - // Run test - ip, err := FallbackLookupIP(context.Background(), host) - // Validate output - assert.NoError(t, err) - assert.ElementsMatch(t, []string{"127.0.0.1"}, ip) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("resolves IPv6 recursively", func(t *testing.T) { - // Setup http mock - defer gock.OffAll() - gock.New("https://1.1.1.1"). - Get("/dns-query"). - MatchParam("name", "api.supabase.com"). - MatchHeader("accept", "application/dns-json"). - Reply(http.StatusOK). - JSON(&cloudflare.DNSResponse{Answer: []cloudflare.DNSAnswer{ - {Type: cloudflare.TypeCNAME, Data: "supabase-api.fly.dev."}, - {Type: cloudflare.TypeAAAA, Data: "2606:2800:220:1:248:1893:25c8:1946"}, - }}) - // Run test - ip, err := FallbackLookupIP(context.Background(), "api.supabase.com") - // Validate output - assert.NoError(t, err) - assert.ElementsMatch(t, []string{"2606:2800:220:1:248:1893:25c8:1946"}, ip) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("returns immediately if already resolved", func(t *testing.T) { - // Run test - ip, err := FallbackLookupIP(context.Background(), "127.0.0.1") - // Validate output - assert.NoError(t, err) - assert.ElementsMatch(t, []string{"127.0.0.1"}, ip) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("empty on network failure", func(t *testing.T) { - // Setup http mock - defer gock.OffAll() - gock.New("https://1.1.1.1"). - Get("/dns-query"). - MatchParam("name", host). - MatchHeader("accept", "application/dns-json"). - ReplyError(errors.New("network error")) - // Run test - ip, err := FallbackLookupIP(context.Background(), host) - // Validate output - assert.ErrorContains(t, err, "network error") - assert.Empty(t, ip) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("empty on service unavailable", func(t *testing.T) { - // Setup http mock - defer gock.OffAll() - gock.New("https://1.1.1.1"). - Get("/dns-query"). - MatchParam("name", host). - MatchHeader("accept", "application/dns-json"). - Reply(http.StatusServiceUnavailable) - // Run test - ip, err := FallbackLookupIP(context.Background(), host) - // Validate output - assert.ErrorContains(t, err, "status 503") - assert.Empty(t, ip) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("empty on malformed json", func(t *testing.T) { - // Setup http mock - defer gock.OffAll() - gock.New("https://1.1.1.1"). - Get("/dns-query"). - MatchParam("name", host). - MatchHeader("accept", "application/dns-json"). - Reply(http.StatusOK). - JSON("malformed") - // Run test - ip, err := FallbackLookupIP(context.Background(), host) - // Validate output - assert.ErrorContains(t, err, "invalid character 'm' looking for beginning of value") - assert.Empty(t, ip) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("empty on no answer", func(t *testing.T) { - // Setup http mock - defer gock.OffAll() - gock.New("https://1.1.1.1"). - Get("/dns-query"). - MatchParam("name", host). - MatchHeader("accept", "application/dns-json"). - Reply(http.StatusOK). - JSON(&cloudflare.DNSResponse{}) - // Run test - ip, err := FallbackLookupIP(context.Background(), host) - // Validate output - assert.ErrorContains(t, err, "failed to locate valid IP for api.supabase.io; resolves to []cloudflare.DNSAnswer(nil)") - assert.Empty(t, ip) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} - -func TestResolveCNAME(t *testing.T) { - t.Run("resolves CNAMEs with CloudFlare", func(t *testing.T) { - defer gock.OffAll() - gock.New("https://1.1.1.1"). - Get("/dns-query"). - MatchParam("name", host). - MatchParam("type", "5"). - MatchHeader("accept", "application/dns-json"). - Reply(http.StatusOK). - JSON(&cloudflare.DNSResponse{Answer: []cloudflare.DNSAnswer{ - {Type: cloudflare.TypeCNAME, Data: "foobarbaz.supabase.co"}, - }}) - // Run test - cname, err := ResolveCNAME(context.Background(), host) - // Validate output - assert.Equal(t, "foobarbaz.supabase.co", cname) - assert.Nil(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("missing CNAMEs return an error", func(t *testing.T) { - defer gock.OffAll() - gock.New("https://1.1.1.1"). - Get("/dns-query"). - MatchParam("name", host). - MatchParam("type", "5"). - MatchHeader("accept", "application/dns-json"). - Reply(http.StatusOK). - JSON(&cloudflare.DNSResponse{Answer: []cloudflare.DNSAnswer{}}) - // Run test - cname, err := ResolveCNAME(context.Background(), host) - // Validate output - assert.Empty(t, cname) - assert.ErrorContains(t, err, "failed to locate appropriate CNAME record for api.supabase.io") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("missing CNAMEs return an error", func(t *testing.T) { - defer gock.OffAll() - gock.New("https://1.1.1.1"). - Get("/dns-query"). - MatchParam("name", host). - MatchParam("type", "5"). - MatchHeader("accept", "application/dns-json"). - Reply(http.StatusOK). - JSON(&cloudflare.DNSResponse{Answer: []cloudflare.DNSAnswer{ - {Type: cloudflare.TypeA, Data: "127.0.0.1"}, - }}) - // Run test - cname, err := ResolveCNAME(context.Background(), host) - // Validate output - assert.Empty(t, cname) - assert.ErrorContains(t, err, "failed to locate appropriate CNAME record for api.supabase.io") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} - -type MockDialer struct { - mock.Mock -} - -func (m *MockDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { - args := m.Called(ctx, network, address) - if conn, ok := args.Get(0).(net.Conn); ok { - return conn, args.Error(1) - } - return nil, args.Error(1) -} - -func TestFallbackDNS(t *testing.T) { - errNetwork := errors.New("network error") - errDNS := &net.DNSError{ - IsTimeout: true, - } - - t.Run("overrides DialContext with DoH", func(t *testing.T) { - DNSResolver.Value = DNS_OVER_HTTPS - // Setup mock dialer - dialer := MockDialer{} - dialer.On("DialContext", mock.Anything, mock.Anything, "127.0.0.1:80"). - Return(nil, errNetwork) - wrapped := withFallbackDNS(dialer.DialContext) - // Setup http mock - defer gock.OffAll() - gock.New("https://1.1.1.1"). - Get("/dns-query"). - MatchParam("name", host). - MatchHeader("accept", "application/dns-json"). - Reply(http.StatusOK). - JSON(&cloudflare.DNSResponse{Answer: []cloudflare.DNSAnswer{ - {Type: cloudflare.TypeA, Data: "127.0.0.1"}, - }}) - // Run test - conn, err := wrapped(context.Background(), "udp", host+":80") - // Check error - assert.ErrorIs(t, err, errNetwork) - assert.Nil(t, conn) - dialer.AssertExpectations(t) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("native with DoH fallback", func(t *testing.T) { - DNSResolver.Value = DNS_GO_NATIVE - // Setup mock dialer - dialer := MockDialer{} - dialer.On("DialContext", mock.Anything, mock.Anything, host+":80"). - Return(nil, errDNS) - dialer.On("DialContext", mock.Anything, mock.Anything, "127.0.0.1:80"). - Return(nil, nil) - wrapped := withFallbackDNS(dialer.DialContext) - // Setup http mock - defer gock.OffAll() - gock.New("https://1.1.1.1"). - Get("/dns-query"). - MatchParam("name", host). - MatchHeader("accept", "application/dns-json"). - Reply(http.StatusOK). - JSON(&cloudflare.DNSResponse{Answer: []cloudflare.DNSAnswer{ - {Type: cloudflare.TypeA, Data: "127.0.0.1"}, - }}) - // Run test - conn, err := wrapped(context.Background(), "udp", host+":80") - // Check error - assert.NoError(t, err) - assert.Nil(t, conn) - dialer.AssertExpectations(t) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on malformed address", func(t *testing.T) { - DNSResolver.Value = DNS_OVER_HTTPS - // Setup mock dialer - dialer := MockDialer{} - wrapped := withFallbackDNS(dialer.DialContext) - // Run test - conn, err := wrapped(context.Background(), "udp", "bad?url") - // Check error - assert.ErrorContains(t, err, "missing port in address") - assert.Nil(t, conn) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on fallback failure", func(t *testing.T) { - DNSResolver.Value = DNS_GO_NATIVE - // Setup mock dialer - dialer := MockDialer{} - dialer.On("DialContext", mock.Anything, mock.Anything, host+":80"). - Return(nil, errDNS) - wrapped := withFallbackDNS(dialer.DialContext) - // Setup http mock - defer gock.OffAll() - gock.New("https://1.1.1.1"). - Get("/dns-query"). - MatchParam("name", host). - MatchHeader("accept", "application/dns-json"). - ReplyError(errNetwork) - // Run test - conn, err := wrapped(context.Background(), "udp", host+":80") - // Check error - assert.ErrorIs(t, err, errDNS) - assert.Nil(t, conn) - dialer.AssertExpectations(t) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} diff --git a/apps/cli-go/internal/utils/cloudflare/api.go b/apps/cli-go/internal/utils/cloudflare/api.go deleted file mode 100644 index 7c7cf10a28..0000000000 --- a/apps/cli-go/internal/utils/cloudflare/api.go +++ /dev/null @@ -1,29 +0,0 @@ -package cloudflare - -import ( - "net/http" - "time" - - "github.com/supabase/cli/pkg/fetcher" -) - -type CloudflareAPI struct { - *fetcher.Fetcher -} - -func NewCloudflareAPI() CloudflareAPI { - server := "https://1.1.1.1" - client := &http.Client{ - Timeout: 10 * time.Second, - } - header := func(req *http.Request) { - req.Header.Add("accept", "application/dns-json") - } - api := CloudflareAPI{Fetcher: fetcher.NewFetcher( - server, - fetcher.WithHTTPClient(client), - fetcher.WithRequestEditor(header), - fetcher.WithExpectedStatus(http.StatusOK), - )} - return api -} diff --git a/apps/cli-go/internal/utils/cloudflare/dns.go b/apps/cli-go/internal/utils/cloudflare/dns.go deleted file mode 100644 index f8a06367cf..0000000000 --- a/apps/cli-go/internal/utils/cloudflare/dns.go +++ /dev/null @@ -1,154 +0,0 @@ -package cloudflare - -import ( - "context" - "net/http" - - "github.com/go-errors/errors" - "github.com/google/go-querystring/query" - "github.com/supabase/cli/pkg/fetcher" -) - -type DNSType uint16 - -// Spec: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4 -// Impl: https://github.com/miekg/dns/blob/master/types.go#L23 -const ( - TypeNone DNSType = 0 - TypeA DNSType = 1 // IPv4 - TypeNS DNSType = 2 - TypeMD DNSType = 3 - TypeMF DNSType = 4 - TypeCNAME DNSType = 5 - TypeSOA DNSType = 6 - TypeMB DNSType = 7 - TypeMG DNSType = 8 - TypeMR DNSType = 9 - TypeNULL DNSType = 10 - TypePTR DNSType = 12 - TypeHINFO DNSType = 13 - TypeMINFO DNSType = 14 - TypeMX DNSType = 15 - TypeTXT DNSType = 16 - TypeRP DNSType = 17 - TypeAFSDB DNSType = 18 - TypeX25 DNSType = 19 - TypeISDN DNSType = 20 - TypeRT DNSType = 21 - TypeNSAPPTR DNSType = 23 - TypeSIG DNSType = 24 - TypeKEY DNSType = 25 - TypePX DNSType = 26 - TypeGPOS DNSType = 27 - TypeAAAA DNSType = 28 // IPv6 - TypeLOC DNSType = 29 - TypeNXT DNSType = 30 - TypeEID DNSType = 31 - TypeNIMLOC DNSType = 32 - TypeSRV DNSType = 33 - TypeATMA DNSType = 34 - TypeNAPTR DNSType = 35 - TypeKX DNSType = 36 - TypeCERT DNSType = 37 - TypeDNAME DNSType = 39 - TypeOPT DNSType = 41 // EDNS - TypeAPL DNSType = 42 - TypeDS DNSType = 43 - TypeSSHFP DNSType = 44 - TypeIPSECKEY DNSType = 45 - TypeRRSIG DNSType = 46 - TypeNSEC DNSType = 47 - TypeDNSKEY DNSType = 48 - TypeDHCID DNSType = 49 - TypeNSEC3 DNSType = 50 - TypeNSEC3PARAM DNSType = 51 - TypeTLSA DNSType = 52 - TypeSMIMEA DNSType = 53 - TypeHIP DNSType = 55 - TypeNINFO DNSType = 56 - TypeRKEY DNSType = 57 - TypeTALINK DNSType = 58 - TypeCDS DNSType = 59 - TypeCDNSKEY DNSType = 60 - TypeOPENPGPKEY DNSType = 61 - TypeCSYNC DNSType = 62 - TypeZONEMD DNSType = 63 - TypeSVCB DNSType = 64 - TypeHTTPS DNSType = 65 - TypeSPF DNSType = 99 - TypeUINFO DNSType = 100 - TypeUID DNSType = 101 - TypeGID DNSType = 102 - TypeUNSPEC DNSType = 103 - TypeNID DNSType = 104 - TypeL32 DNSType = 105 - TypeL64 DNSType = 106 - TypeLP DNSType = 107 - TypeEUI48 DNSType = 108 - TypeEUI64 DNSType = 109 - TypeURI DNSType = 256 - TypeCAA DNSType = 257 - TypeAVC DNSType = 258 - TypeAMTRELAY DNSType = 260 - - TypeTKEY DNSType = 249 - TypeTSIG DNSType = 250 - - // valid Question.Qtype only - TypeIXFR DNSType = 251 - TypeAXFR DNSType = 252 - TypeMAILB DNSType = 253 - TypeMAILA DNSType = 254 - TypeANY DNSType = 255 - - TypeTA DNSType = 32768 - TypeDLV DNSType = 32769 - TypeReserved DNSType = 65535 -) - -type DNSQuestion struct { - Name string `json:"name"` - Type DNSType `json:"type"` -} - -type DNSAnswer struct { - Name string `json:"name"` - Type DNSType `json:"type"` - Ttl uint32 `json:"TTL"` - Data string `json:"data"` -} - -// Ref: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-6 -type DNSResponse struct { - Status uint16 - TC bool - RD bool - RA bool - AD bool - CD bool - Question []DNSQuestion `json:",omitempty"` - Answer []DNSAnswer `json:",omitempty"` - Authority []DNSAnswer `json:",omitempty"` - Additional []DNSAnswer `json:",omitempty"` -} - -type DNSParams struct { - Name string `url:"name"` - Type *DNSType `url:"type,omitempty"` - Do *bool `url:"do,omitempty"` - Cd *bool `url:"cd,omitempty"` -} - -// Performs DNS lookup via HTTPS, in case firewall blocks native netgo resolver. -// Ref: https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json -func (c *CloudflareAPI) DNSQuery(ctx context.Context, params DNSParams) (DNSResponse, error) { - values, err := query.Values(params) - if err != nil { - return DNSResponse{}, errors.Errorf("failed to encode query params: %w", err) - } - resp, err := c.Send(ctx, http.MethodGet, "/dns-query?"+values.Encode(), nil) - if err != nil { - return DNSResponse{}, err - } - return fetcher.ParseJSON[DNSResponse](resp.Body) -} diff --git a/apps/cli-go/internal/utils/cloudflare/dns_test.go b/apps/cli-go/internal/utils/cloudflare/dns_test.go deleted file mode 100644 index ee4d00c299..0000000000 --- a/apps/cli-go/internal/utils/cloudflare/dns_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package cloudflare - -import ( - "context" - "net/http" - "testing" - - "github.com/h2non/gock" - "github.com/stretchr/testify/assert" - "github.com/supabase/cli/internal/testing/apitest" -) - -func TestDNSQuery(t *testing.T) { - t.Run("successfully queries A records", func(t *testing.T) { - api := NewCloudflareAPI() - defer gock.OffAll() - gock.New("https://1.1.1.1"). - Get("/dns-query"). - MatchParam("name", "example.com"). - MatchHeader("accept", "application/dns-json"). - Reply(http.StatusOK). - JSON(DNSResponse{ - Answer: []DNSAnswer{ - { - Name: "example.com", - Type: TypeA, - Ttl: 300, - Data: "93.184.216.34", - }, - }, - }) - - resp, err := api.DNSQuery(context.Background(), DNSParams{ - Name: "example.com", - }) - - assert.NoError(t, err) - assert.Len(t, resp.Answer, 1) - assert.Equal(t, "93.184.216.34", resp.Answer[0].Data) - assert.Equal(t, TypeA, resp.Answer[0].Type) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("successfully queries specific DNS type", func(t *testing.T) { - api := NewCloudflareAPI() - dnsType := TypeCNAME - defer gock.OffAll() - gock.New("https://1.1.1.1"). - Get("/dns-query"). - MatchParam("name", "www.example.com"). - MatchParam("type", "5"). // TypeCNAME = 5 - MatchHeader("accept", "application/dns-json"). - Reply(http.StatusOK). - JSON(DNSResponse{ - Answer: []DNSAnswer{ - { - Name: "www.example.com", - Type: TypeCNAME, - Ttl: 3600, - Data: "example.com", - }, - }, - }) - - resp, err := api.DNSQuery(context.Background(), DNSParams{ - Name: "www.example.com", - Type: &dnsType, - }) - - assert.NoError(t, err) - assert.Len(t, resp.Answer, 1) - assert.Equal(t, "example.com", resp.Answer[0].Data) - assert.Equal(t, TypeCNAME, resp.Answer[0].Type) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("handles network error", func(t *testing.T) { - api := NewCloudflareAPI() - defer gock.OffAll() - gock.New("https://1.1.1.1"). - Get("/dns-query"). - MatchParam("name", "example.com"). - ReplyError(gock.ErrCannotMatch) - - resp, err := api.DNSQuery(context.Background(), DNSParams{ - Name: "example.com", - }) - - assert.Error(t, err) - assert.Empty(t, resp.Answer) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("handles service unavailable", func(t *testing.T) { - api := NewCloudflareAPI() - defer gock.OffAll() - gock.New("https://1.1.1.1"). - Get("/dns-query"). - MatchParam("name", "example.com"). - Reply(http.StatusServiceUnavailable) - - resp, err := api.DNSQuery(context.Background(), DNSParams{ - Name: "example.com", - }) - - assert.ErrorContains(t, err, "status 503") - assert.Empty(t, resp.Answer) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("handles malformed response", func(t *testing.T) { - api := NewCloudflareAPI() - defer gock.OffAll() - gock.New("https://1.1.1.1"). - Get("/dns-query"). - MatchParam("name", "example.com"). - Reply(http.StatusOK). - JSON("invalid json") - - resp, err := api.DNSQuery(context.Background(), DNSParams{ - Name: "example.com", - }) - - assert.ErrorContains(t, err, "failed to parse") - assert.Empty(t, resp.Answer) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} diff --git a/apps/cli-go/internal/utils/colors.go b/apps/cli-go/internal/utils/colors.go deleted file mode 100644 index f4f82652c0..0000000000 --- a/apps/cli-go/internal/utils/colors.go +++ /dev/null @@ -1,28 +0,0 @@ -package utils - -import ( - "github.com/charmbracelet/lipgloss" -) - -// For commands & names. -func Aqua(str string) string { - return lipgloss.NewStyle().Foreground(lipgloss.Color("14")).Render(str) -} - -func Yellow(str string) string { - return lipgloss.NewStyle().Foreground(lipgloss.Color("11")).Render(str) -} - -func Green(str string) string { - return lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Render(str) -} - -// For errors. -func Red(str string) string { - return lipgloss.NewStyle().Foreground(lipgloss.Color("9")).Render(str) -} - -// For paths & filenames. -func Bold(str string) string { - return lipgloss.NewStyle().Bold(true).Render(str) -} diff --git a/apps/cli-go/internal/utils/config.go b/apps/cli-go/internal/utils/config.go deleted file mode 100644 index 2ec13b26aa..0000000000 --- a/apps/cli-go/internal/utils/config.go +++ /dev/null @@ -1,268 +0,0 @@ -package utils - -import ( - _ "embed" - "io/fs" - "net" - "net/url" - "os" - "strconv" - - "github.com/compose-spec/compose-go/v2/types" - "github.com/go-errors/errors" - "github.com/spf13/afero" - "github.com/spf13/viper" - "github.com/supabase/cli/pkg/config" -) - -var ( - NetId string - DbId string - KongId string - GotrueId string - InbucketId string - RealtimeId string - RestId string - StorageId string - ImgProxyId string - DifferId string - PgmetaId string - StudioId string - EdgeRuntimeId string - LogflareId string - VectorId string - PoolerId string - - DbAliases = []string{"db", "db.supabase.internal"} - KongAliases = []string{"kong", "api.supabase.internal"} - GotrueAliases = []string{"auth"} - InbucketAliases = []string{"inbucket"} - RealtimeAliases = []string{"realtime", Config.Realtime.TenantId} - RestAliases = []string{"rest"} - StorageAliases = []string{"storage"} - ImgProxyAliases = []string{"imgproxy"} - PgmetaAliases = []string{"pg_meta"} - StudioAliases = []string{"studio"} - EdgeRuntimeAliases = []string{"edge_runtime"} - LogflareAliases = []string{"analytics"} - VectorAliases = []string{"vector"} - PoolerAliases = []string{"pooler"} - - //go:embed templates/initial_schemas/13.sql - InitialSchemaPg13Sql string - //go:embed templates/initial_schemas/14.sql - InitialSchemaPg14Sql string -) - -func GetId(name string) string { - return "supabase_" + name + "_" + Config.ProjectId -} - -func UpdateDockerIds() { - if NetId = viper.GetString("network-id"); len(NetId) == 0 { - NetId = GetId("network") - } - DbId = GetId(DbAliases[0]) - KongId = GetId(KongAliases[0]) - GotrueId = GetId(GotrueAliases[0]) - InbucketId = GetId(InbucketAliases[0]) - RealtimeId = GetId(RealtimeAliases[0]) - RestId = GetId(RestAliases[0]) - StorageId = GetId(StorageAliases[0]) - ImgProxyId = GetId(ImgProxyAliases[0]) - DifferId = GetId("differ") - PgmetaId = GetId(PgmetaAliases[0]) - StudioId = GetId(StudioAliases[0]) - EdgeRuntimeId = GetId(EdgeRuntimeAliases[0]) - LogflareId = GetId(LogflareAliases[0]) - VectorId = GetId(VectorAliases[0]) - PoolerId = GetId(PoolerAliases[0]) -} - -func GetDockerIds() []string { - return []string{ - KongId, - GotrueId, - InbucketId, - RealtimeId, - RestId, - StorageId, - ImgProxyId, - PgmetaId, - StudioId, - EdgeRuntimeId, - LogflareId, - VectorId, - PoolerId, - } -} - -var Config = config.NewConfig(config.WithHostname(GetHostname())) - -func GetServices() types.Services { - services := types.Services{ - "db": { - Name: ShortContainerImageName(Config.Db.Image), - Image: GetRegistryImageUrl(Config.Db.Image), - PullPolicy: types.PullPolicyMissing, - }, - } - if Config.Api.Enabled { - services["gateway"] = types.ServiceConfig{ - Name: ShortContainerImageName(Config.Api.KongImage), - Image: GetRegistryImageUrl(Config.Api.KongImage), - PullPolicy: types.PullPolicyMissing, - } - services["api"] = types.ServiceConfig{ - Name: ShortContainerImageName(Config.Api.Image), - Image: GetRegistryImageUrl(Config.Api.Image), - PullPolicy: types.PullPolicyMissing, - } - } - if Config.Auth.Enabled { - services["auth"] = types.ServiceConfig{ - Name: ShortContainerImageName(Config.Auth.Image), - Image: GetRegistryImageUrl(Config.Auth.Image), - PullPolicy: types.PullPolicyMissing, - } - } - if Config.Inbucket.Enabled { - services["mailpit"] = types.ServiceConfig{ - Name: ShortContainerImageName(Config.Inbucket.Image), - Image: GetRegistryImageUrl(Config.Inbucket.Image), - PullPolicy: types.PullPolicyMissing, - } - } - if Config.Realtime.Enabled { - services["realtime"] = types.ServiceConfig{ - Name: ShortContainerImageName(Config.Realtime.Image), - Image: GetRegistryImageUrl(Config.Realtime.Image), - PullPolicy: types.PullPolicyMissing, - } - } - if Config.Storage.Enabled { - services["storage"] = types.ServiceConfig{ - Name: ShortContainerImageName(Config.Storage.Image), - Image: GetRegistryImageUrl(Config.Storage.Image), - PullPolicy: types.PullPolicyMissing, - } - services["imgProxy"] = types.ServiceConfig{ - Name: ShortContainerImageName(Config.Storage.ImgProxyImage), - Image: GetRegistryImageUrl(Config.Storage.ImgProxyImage), - PullPolicy: types.PullPolicyMissing, - } - } - if Config.EdgeRuntime.Enabled { - services["edgeRuntime"] = types.ServiceConfig{ - Name: ShortContainerImageName(Config.EdgeRuntime.Image), - Image: GetRegistryImageUrl(Config.EdgeRuntime.Image), - PullPolicy: types.PullPolicyMissing, - } - } - if Config.Studio.Enabled { - services["studio"] = types.ServiceConfig{ - Name: ShortContainerImageName(Config.Studio.Image), - Image: GetRegistryImageUrl(Config.Studio.Image), - PullPolicy: types.PullPolicyMissing, - } - services["pgmeta"] = types.ServiceConfig{ - Name: ShortContainerImageName(Config.Studio.PgmetaImage), - Image: GetRegistryImageUrl(Config.Studio.PgmetaImage), - PullPolicy: types.PullPolicyMissing, - } - } - if Config.Analytics.Enabled { - services["analytics"] = types.ServiceConfig{ - Name: ShortContainerImageName(Config.Analytics.Image), - Image: GetRegistryImageUrl(Config.Analytics.Image), - PullPolicy: types.PullPolicyMissing, - } - services["vector"] = types.ServiceConfig{ - Name: ShortContainerImageName(Config.Analytics.VectorImage), - Image: GetRegistryImageUrl(Config.Analytics.VectorImage), - PullPolicy: types.PullPolicyMissing, - } - } - if Config.Db.Pooler.Enabled { - services["pooler"] = types.ServiceConfig{ - Name: ShortContainerImageName(Config.Db.Pooler.Image), - Image: GetRegistryImageUrl(Config.Db.Pooler.Image), - PullPolicy: types.PullPolicyMissing, - } - } - return services -} - -// Adapts fs.FS to support absolute paths -type rootFS struct { - fsys afero.Fs -} - -func (f *rootFS) Open(name string) (fs.File, error) { - return f.fsys.Open(name) -} - -func NewRootFS(fsys afero.Fs) fs.FS { - return &rootFS{fsys: fsys} -} - -func ToRealtimeEnv(addr config.AddressFamily) string { - if addr == config.AddressIPv6 { - return "-proto_dist inet6_tcp" - } - return "-proto_dist inet_tcp" -} - -type InitParams struct { - ProjectId string - UseOrioleDB bool - UsePgDelta bool - Overwrite bool -} - -func InitConfig(params InitParams, fsys afero.Fs) error { - c := config.NewConfig() - c.ProjectId = params.ProjectId - if params.UseOrioleDB { - c.Db.OrioleDBVersion = "17.11.0.002" - } - // The supabase init command opts new projects into pg-delta. Existing configs are - // unaffected because mergeDefaultValues ejects with this flag false (default stays - // migra), and other InitConfig callers leave it disabled. - c.Experimental.PgDeltaInitEnabled = params.UsePgDelta - // Create config file - if err := MkdirIfNotExistFS(fsys, SupabaseDirPath); err != nil { - return err - } - flag := os.O_WRONLY | os.O_CREATE - if params.Overwrite { - flag |= os.O_TRUNC - } else { - flag |= os.O_EXCL - } - f, err := fsys.OpenFile(ConfigPath, flag, 0644) - if err != nil { - return errors.Errorf("failed to create config file: %w", err) - } - defer f.Close() - return c.Eject(f) -} - -func WriteConfig(fsys afero.Fs, _test bool) error { - return InitConfig(InitParams{}, fsys) -} - -func GetApiUrl(path string) string { - if len(Config.Api.ExternalUrl) > 0 { - return Config.Api.ExternalUrl + path - } - hostPort := net.JoinHostPort(Config.Hostname, - strconv.FormatUint(uint64(Config.Api.Port), 10), - ) - apiUrl := url.URL{ - Scheme: "http", - Host: hostPort, - Path: path, - } - return apiUrl.String() -} diff --git a/apps/cli-go/internal/utils/config_test.go b/apps/cli-go/internal/utils/config_test.go deleted file mode 100644 index 98cd4da9e8..0000000000 --- a/apps/cli-go/internal/utils/config_test.go +++ /dev/null @@ -1,197 +0,0 @@ -package utils - -import ( - "os" - "testing" - - "github.com/spf13/afero" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGetId(t *testing.T) { - t.Run("generates container id", func(t *testing.T) { - Config.ProjectId = "test-project" - name := "test-service" - - id := GetId(name) - - assert.Equal(t, "supabase_test-service_test-project", id) - }) -} - -func TestUpdateDockerIds(t *testing.T) { - t.Run("updates all container ids", func(t *testing.T) { - Config.ProjectId = "test-project" - viper.Set("network-id", "custom-network") - defer viper.Reset() - - UpdateDockerIds() - - assert.Equal(t, "custom-network", NetId) - assert.Equal(t, "supabase_db_test-project", DbId) - assert.Equal(t, "supabase_kong_test-project", KongId) - assert.Equal(t, "supabase_auth_test-project", GotrueId) - assert.Equal(t, "supabase_inbucket_test-project", InbucketId) - assert.Equal(t, "supabase_realtime_test-project", RealtimeId) - assert.Equal(t, "supabase_rest_test-project", RestId) - assert.Equal(t, "supabase_storage_test-project", StorageId) - assert.Equal(t, "supabase_imgproxy_test-project", ImgProxyId) - assert.Equal(t, "supabase_differ_test-project", DifferId) - assert.Equal(t, "supabase_pg_meta_test-project", PgmetaId) - assert.Equal(t, "supabase_studio_test-project", StudioId) - assert.Equal(t, "supabase_edge_runtime_test-project", EdgeRuntimeId) - assert.Equal(t, "supabase_analytics_test-project", LogflareId) - assert.Equal(t, "supabase_vector_test-project", VectorId) - assert.Equal(t, "supabase_pooler_test-project", PoolerId) - }) - - t.Run("generates network id if not set", func(t *testing.T) { - Config.ProjectId = "test-project" - viper.Reset() - - UpdateDockerIds() - - assert.Equal(t, "supabase_network_test-project", NetId) - }) -} - -func TestInitConfig(t *testing.T) { - t.Run("creates new config file", func(t *testing.T) { - fsys := afero.NewMemMapFs() - params := InitParams{ - ProjectId: "test-project", - } - - err := InitConfig(params, fsys) - - assert.NoError(t, err) - exists, err := afero.Exists(fsys, ConfigPath) - assert.NoError(t, err) - assert.True(t, exists) - }) - - t.Run("generated config enables pgdelta when requested", func(t *testing.T) { - fsys := afero.NewMemMapFs() - params := InitParams{ - ProjectId: "test-project", - UsePgDelta: true, - } - - err := InitConfig(params, fsys) - - require.NoError(t, err) - content, err := afero.ReadFile(fsys, ConfigPath) - require.NoError(t, err) - assert.Contains(t, string(content), "[experimental.pgdelta]\nenabled = true") - }) - - t.Run("generated config leaves pgdelta disabled by default", func(t *testing.T) { - fsys := afero.NewMemMapFs() - params := InitParams{ - ProjectId: "test-project", - } - - err := InitConfig(params, fsys) - - require.NoError(t, err) - content, err := afero.ReadFile(fsys, ConfigPath) - require.NoError(t, err) - assert.Contains(t, string(content), "[experimental.pgdelta]\nenabled = false") - }) - - t.Run("creates config with orioledb", func(t *testing.T) { - fsys := afero.NewMemMapFs() - params := InitParams{ - ProjectId: "test-project", - UseOrioleDB: true, - } - - err := InitConfig(params, fsys) - - assert.NoError(t, err) - content, err := afero.ReadFile(fsys, ConfigPath) - assert.NoError(t, err) - assert.Contains(t, string(content), "17.11.0.002") - }) - - t.Run("fails if config exists and no overwrite", func(t *testing.T) { - fsys := afero.NewMemMapFs() - err := afero.WriteFile(fsys, ConfigPath, []byte("existing"), 0644) - require.NoError(t, err) - params := InitParams{ - ProjectId: "test-project", - } - - err = InitConfig(params, fsys) - - assert.ErrorIs(t, err, os.ErrExist) - }) - - t.Run("overwrites existing config when specified", func(t *testing.T) { - fsys := afero.NewMemMapFs() - err := afero.WriteFile(fsys, ConfigPath, []byte("existing"), 0644) - require.NoError(t, err) - params := InitParams{ - ProjectId: "test-project", - Overwrite: true, - } - - err = InitConfig(params, fsys) - - assert.NoError(t, err) - content, err := afero.ReadFile(fsys, ConfigPath) - assert.NoError(t, err) - assert.NotEqual(t, "existing", string(content)) - }) -} - -func TestGetApiUrl(t *testing.T) { - t.Run("returns external url when configured", func(t *testing.T) { - Config.Api.ExternalUrl = "https://api.example.com" - - url := GetApiUrl("/test") - - assert.Equal(t, "https://api.example.com/test", url) - }) - - t.Run("builds url from hostname and port", func(t *testing.T) { - Config.Hostname = "localhost" - Config.Api.Port = 8000 - Config.Api.ExternalUrl = "" - - url := GetApiUrl("/test") - - assert.Equal(t, "http://localhost:8000/test", url) - }) -} - -func TestRootFS(t *testing.T) { - t.Run("opens file from root fs", func(t *testing.T) { - fsys := afero.NewMemMapFs() - content := []byte("test content") - err := afero.WriteFile(fsys, "/test.txt", content, 0644) - require.NoError(t, err) - - rootfs := NewRootFS(fsys) - f, err := rootfs.Open("/test.txt") - assert.NoError(t, err) - defer f.Close() - - buf := make([]byte, len(content)) - n, err := f.Read(buf) - assert.NoError(t, err) - assert.Equal(t, len(content), n) - assert.Equal(t, content, buf) - }) - - t.Run("returns error for non-existent file", func(t *testing.T) { - fsys := afero.NewMemMapFs() - rootfs := NewRootFS(fsys) - - _, err := rootfs.Open("/non-existent.txt") - - assert.Error(t, err) - }) -} diff --git a/apps/cli-go/internal/utils/connect.go b/apps/cli-go/internal/utils/connect.go deleted file mode 100644 index ec31067d6d..0000000000 --- a/apps/cli-go/internal/utils/connect.go +++ /dev/null @@ -1,386 +0,0 @@ -package utils - -import ( - "context" - "fmt" - "io" - "net" - "net/url" - "os" - "regexp" - "strings" - "time" - - "github.com/go-errors/errors" - "github.com/jackc/pgconn" - "github.com/jackc/pgx/v4" - "github.com/spf13/afero" - "github.com/spf13/viper" - "github.com/supabase/cli/internal/debug" - "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/pgxv5" - "golang.org/x/net/publicsuffix" -) - -func ToPostgresURL(config pgconn.Config) string { - return toPostgresURL(config, url.UserPassword(config.User, config.Password)) -} - -func toPostgresURL(config pgconn.Config, userinfo *url.Userinfo) string { - timeoutSecond := int64(config.ConnectTimeout.Seconds()) - if timeoutSecond == 0 { - timeoutSecond = 10 - } - queryParams := fmt.Sprintf("connect_timeout=%d", timeoutSecond) - for k, v := range config.RuntimeParams { - queryParams += fmt.Sprintf("&%s=%s", k, url.QueryEscape(v)) - } - // IPv6 address must be wrapped in square brackets - host := config.Host - if ip := net.ParseIP(host); ip != nil && ip.To4() == nil { - host = fmt.Sprintf("[%s]", host) - } - return fmt.Sprintf( - "postgresql://%s@%s:%d/%s?%s", - userinfo, - host, - config.Port, - url.PathEscape(config.Database), - queryParams, - ) -} - -var ErrPrimaryNotFound = errors.New("primary database not found") - -func GetPoolerConfigPrimary(ctx context.Context, ref string) (api.SupavisorConfigResponseOutput, error) { - var result api.SupavisorConfigResponseOutput - resp, err := GetSupabase().V1GetPoolerConfigWithResponse(ctx, ref) - if err != nil { - return result, errors.Errorf("failed to get pooler: %w", err) - } else if resp.JSON200 == nil { - return result, errors.Errorf("unexpected get pooler status %d: %s", resp.StatusCode(), string(resp.Body)) - } - for _, config := range *resp.JSON200 { - if config.DatabaseType == api.SupavisorConfigResponseOutputDatabaseTypePRIMARY { - return config, nil - } - } - return result, errors.New(ErrPrimaryNotFound) -} - -func GetPoolerConfig(projectRef string) *pgconn.Config { - logger := GetDebugLogger() - if len(Config.Db.Pooler.ConnectionString) == 0 { - fmt.Fprintln(logger, "Pooler URL is not configured") - return nil - } - poolerConfig, err := ParsePoolerURL(Config.Db.Pooler.ConnectionString) - if err != nil { - fmt.Fprintln(logger, err) - return nil - } - if poolerConfig.RuntimeParams == nil { - poolerConfig.RuntimeParams = make(map[string]string) - } - // Verify that the pooler username matches the database host being connected to - if _, ref, found := strings.Cut(poolerConfig.User, "."); !found { - for option := range strings.SplitSeq(poolerConfig.RuntimeParams["options"], ",") { - key, value, found := strings.Cut(option, "=") - if found && key == "reference" && value != projectRef { - fmt.Fprintln(logger, "Pooler options does not match project ref:", projectRef) - return nil - } - } - } else if projectRef != ref { - fmt.Fprintln(logger, "Pooler username does not match project ref:", projectRef) - return nil - } - // There is a risk of MITM attack if we simply trust the hostname specified in pooler URL. - if err := assertDomainInProfile(poolerConfig.Host); err != nil { - fmt.Fprintln(logger, err) - return nil - } - fmt.Fprintln(logger, "Using connection pooler:", Config.Db.Pooler.ConnectionString) - // Supavisor transaction mode does not support prepared statement - poolerConfig.Port = 5432 - return poolerConfig -} - -func ParsePoolerURL(connString string) (*pgconn.Config, error) { - // Remove password from pooler connection string because the placeholder text - // [YOUR-PASSWORD] messes up pgconn.ParseConfig. The password must be percent - // escaped so we cannot simply call strings.Replace with actual password. - poolerUrl := strings.ReplaceAll(connString, "[YOUR-PASSWORD]", "") - poolerConfig, err := pgconn.ParseConfig(poolerUrl) - if err != nil { - return nil, errors.Errorf("failed to parse pooler URL: %w", err) - } - return poolerConfig, nil -} - -func assertDomainInProfile(host string) error { - domain, err := publicsuffix.EffectiveTLDPlusOne(host) - if err != nil { - return errors.Errorf("failed to parse pooler TLD: %w", err) - } - if len(CurrentProfile.PoolerHost) > 0 && !strings.EqualFold(CurrentProfile.PoolerHost, domain) { - return errors.Errorf("Pooler domain does not belong to current profile: %s", domain) - } - return nil -} - -// Connnect to local Postgres with optimised settings. The caller is responsible for closing the connection returned. -func ConnectLocalPostgres(ctx context.Context, config pgconn.Config, options ...func(*pgx.ConnConfig)) (*pgx.Conn, error) { - if len(config.Host) == 0 { - config.Host = Config.Hostname - } - if config.Port == 0 { - config.Port = Config.Db.Port - } - if len(config.User) == 0 { - config.User = "postgres" - } - if len(config.Password) == 0 { - config.Password = Config.Db.Password - } - if len(config.Database) == 0 { - config.Database = "postgres" - } - if config.ConnectTimeout == 0 { - config.ConnectTimeout = 2 * time.Second - } - options = append(options, func(cc *pgx.ConnConfig) { - cc.TLSConfig = nil - }) - return ConnectByUrl(ctx, ToPostgresURL(config), options...) -} - -func ConnectByUrl(ctx context.Context, url string, options ...func(*pgx.ConnConfig)) (*pgx.Conn, error) { - if viper.GetBool("DEBUG") { - options = append(options, debug.SetupPGX) - } - // No fallback from TLS to unsecure connection - options = append(options, func(cc *pgx.ConnConfig) { - if cc.TLSConfig == nil { - return - } - var fallbacks []*pgconn.FallbackConfig - for _, fc := range cc.Fallbacks { - if fc.TLSConfig != nil { - fallbacks = append(fallbacks, fc) - } - } - cc.Fallbacks = fallbacks - }) - conn, err := pgxv5.Connect(ctx, url, options...) - SetConnectSuggestion(err) - return conn, err -} - -const SuggestEnvVar = "Connect to your database by setting the env var correctly: SUPABASE_DB_PASSWORD" - -// ipv6LiteralPattern matches IPv6 addresses in connection errors, e.g. Go dial -// "dial tcp [2406:da18:...]:5432" or libpq -// `connection to server at "host" (2406:da18:...), port 5432 failed`. -var ipv6LiteralPattern = regexp.MustCompile(`(?:\[[0-9a-fA-F:]+\]|\([0-9a-fA-F:]+\))`) - -// isIPv6ConnectivityError reports whether the connection failure stems from the -// host resolving to an IPv6 address that the current network cannot route to. -// Supabase direct database connections (db..supabase.co:5432) are -// IPv6-only unless the IPv4 add-on is enabled, so users on IPv4-only networks -// (or inside a Docker container without an IPv6 stack, e.g. `supabase db dump`) -// hit these failures. -func isIPv6ConnectivityError(msg string) bool { - lower := strings.ToLower(msg) - switch { - case strings.Contains(lower, "address family for hostname not supported"), - strings.Contains(lower, "no address associated with hostname"): - // getaddrinfo inside the pg_dump container when the host is IPv6-only and - // the container has no IPv6 stack, so AI_ADDRCONFIG filters out the AAAA - // record: "could not translate host name ... to address: Address family - // for hostname not supported" / "... No address associated with hostname". - return true - case strings.Contains(lower, "network is unreachable"): - return true - case strings.Contains(lower, "no route to host"), - strings.Contains(lower, "cannot assign requested address"): - // Require an IPv6 literal so genuine project-not-found errors (which the - // branch below maps) keep their existing suggestion. - return ipv6LiteralPattern.MatchString(msg) - } - return false -} - -// IsIPv6ConnectivityError reports whether err is a database connection failure -// caused by an IPv6 address the current network (or container) cannot reach. -func IsIPv6ConnectivityError(err error) bool { - if err == nil { - return false - } - return isIPv6ConnectivityError(err.Error()) -} - -// ipv6Suggestion is the generic, command-agnostic hint shown when a direct -// connection fails because the host is IPv6-only. It points users at the IPv4 -// transaction pooler via --db-url; SuggestIPv6Pooler upgrades it with the -// project's actual connection string when one can be fetched. -func ipv6Suggestion() string { - return fmt.Sprintf( - "Your network does not support IPv6, which is required for direct connections to the database.\n"+ - "Retry with your project's IPv4 transaction pooler connection string via %s.\n"+ - "You can copy it from the dashboard under Connect > Transaction pooler.", - Aqua("--db-url"), - ) -} - -// poolerURLPasswordPattern captures the userinfo password of a postgres -// connection string (the bytes between "user:" and the "@" host separator). -var poolerURLPasswordPattern = regexp.MustCompile(`^(postgres(?:ql)?://[^:@/]+:)[^@]*@`) - -// maskPoolerPassword replaces the password in a pooler connection string with -// the [YOUR-PASSWORD] placeholder. The Management API may return a real password -// in connection_string, and the suggestion is printed to the terminal, so the -// password must never be echoed. The placeholder keeps the hint copy-pasteable. -func maskPoolerPassword(connString string) string { - return poolerURLPasswordPattern.ReplaceAllString(connString, "${1}[YOUR-PASSWORD]@") -} - -// ipv6PoolerSuggestion is the IPv6 hint enriched with the project's transaction -// pooler connection string (password masked), ready to paste into --db-url. -func ipv6PoolerSuggestion(connString string) string { - return fmt.Sprintf( - "Your network does not support IPv6, which is required for direct connections to the database.\n"+ - "Retry through the IPv4 transaction pooler by passing it to %s", - Aqua(fmt.Sprintf(`--db-url "%s"`, maskPoolerPassword(connString))), - ) -} - -// ProjectRefFromDirectDbHost extracts the project ref from a Supabase direct -// database host (db..supabase.co|red). It returns false for any other host, -// including pooler hosts and local databases. -func ProjectRefFromDirectDbHost(host string) (string, bool) { - matches := ProjectHostPattern.FindStringSubmatch(host) - if len(matches) < 3 { - return "", false - } - return matches[2], true -} - -// WarnIPv6PoolerFallback prints a user-visible warning explaining that the direct -// database connection could not be used because the current environment does not -// support IPv6, and that the CLI is retrying through the IPv4 connection pooler. -func WarnIPv6PoolerFallback(directHost string) { - fmt.Fprintln(os.Stderr, Yellow(fmt.Sprintf( - "Warning: Direct connection to %s is unavailable because this environment does not support IPv6.\n"+ - "Retrying via the IPv4 connection pooler.", - directHost, - ))) -} - -// SuggestIPv6Pooler upgrades CmdSuggestion with the project's transaction pooler -// connection string when host is a Supabase direct database host and the pooler -// config can be fetched. Returns true when the suggestion was set. -func SuggestIPv6Pooler(ctx context.Context, host string) bool { - ref, ok := ProjectRefFromDirectDbHost(host) - if !ok { - return false - } - // GetSupabase() fatally exits when no access token is configured, so only - // reach for the API when a token is available (e.g. --db-url without login). - if _, err := LoadAccessTokenFS(afero.NewOsFs()); err != nil { - return false - } - primary, err := GetPoolerConfigPrimary(ctx, ref) - if err != nil || len(primary.ConnectionString) == 0 { - return false - } - CmdSuggestion = ipv6PoolerSuggestion(primary.ConnectionString) - return true -} - -// Sets CmdSuggestion to an actionable hint based on the given pg connection error. -func SetConnectSuggestion(err error) { - if err == nil { - return - } - msg := err.Error() - if strings.Contains(msg, "connect: connection refused") || - strings.Contains(msg, "Address not in tenant allow_list") { - CmdSuggestion = fmt.Sprintf( - "Make sure your local IP is allowed in Network Restrictions and Network Bans.\n%s/project/_/database/settings", - CurrentProfile.DashboardURL, - ) - } else if strings.Contains(msg, "SCRAM exchange: Wrong password") || strings.Contains(msg, "failed SASL auth") { - // password authentication failed for user / invalid SCRAM server-final-message received from server - CmdSuggestion = SuggestEnvVar - } else if isIPv6ConnectivityError(msg) { - CmdSuggestion = ipv6Suggestion() - } else if strings.Contains(msg, "connect: no route to host") || strings.Contains(msg, "Tenant or user not found") { - // Assumes IPv6 check has been performed before this - CmdSuggestion = "Make sure your project exists on profile: " + CurrentProfile.Name - } -} - -const ( - SUPERUSER_ROLE = "supabase_admin" - CLI_LOGIN_PREFIX = "cli_login_" - SET_SESSION_ROLE = "SET SESSION ROLE postgres" -) - -func ConnectByConfigStream(ctx context.Context, config pgconn.Config, w io.Writer, options ...func(*pgx.ConnConfig)) (*pgx.Conn, error) { - if IsLocalDatabase(config) { - fmt.Fprintln(w, "Connecting to local database...") - return ConnectLocalPostgres(ctx, config, options...) - } - fmt.Fprintln(w, "Connecting to remote database...") - // Ahead of the caller's overrides so they still win, e.g. pgtest clearing TLS. - opts := append([]func(*pgx.ConnConfig){preserveTLSConfig(config)}, options...) - opts = append(opts, func(cc *pgx.ConnConfig) { - if DNSResolver.Value == DNS_OVER_HTTPS { - cc.LookupFunc = FallbackLookupIP - } - // Step down from platform provisioned login roles or privileged roles - if user := strings.Split(cc.User, ".")[0]; strings.EqualFold(user, SUPERUSER_ROLE) || - strings.HasPrefix(user, CLI_LOGIN_PREFIX) { - cc.AfterConnect = func(ctx context.Context, pgconn *pgconn.PgConn) error { - return pgconn.Exec(ctx, SET_SESSION_ROLE).Close() - } - } - }) - return ConnectByUrl(ctx, ToPostgresURL(config), opts...) -} - -// preserveTLSConfig replays the TLS state pgconn resolved from sslmode, which -// ToPostgresURL cannot serialize (it lives in TLSConfig/Fallbacks, not -// RuntimeParams) so the rebuilt URL re-resolves it from PGSSLMODE and libpq's -// "prefer" default. Nil Fallbacks, which ParseConfig never produces, marks a -// config assembled in code with no preference to replay. -func preserveTLSConfig(config pgconn.Config) func(*pgx.ConnConfig) { - return func(cc *pgx.ConnConfig) { - if config.Fallbacks == nil { - return - } - cc.TLSConfig = config.TLSConfig - // Rebuild against the URL's endpoint: callers may override the port - // (GetPoolerConfig pins 5432), and extra HA hosts are dropped anyway. - cc.Fallbacks = nil - for _, fb := range config.Fallbacks { - if fb.Host != config.Host { - break - } - cc.Fallbacks = append(cc.Fallbacks, &pgconn.FallbackConfig{ - Host: cc.Host, - Port: cc.Port, - TLSConfig: fb.TLSConfig, - }) - } - } -} - -func ConnectByConfig(ctx context.Context, config pgconn.Config, options ...func(*pgx.ConnConfig)) (*pgx.Conn, error) { - return ConnectByConfigStream(ctx, config, os.Stderr, options...) -} - -func IsLocalDatabase(config pgconn.Config) bool { - return config.Host == Config.Hostname && (config.Port == Config.Db.Port || config.Port == Config.Db.ShadowPort) -} diff --git a/apps/cli-go/internal/utils/connect_test.go b/apps/cli-go/internal/utils/connect_test.go deleted file mode 100644 index b6ea142d91..0000000000 --- a/apps/cli-go/internal/utils/connect_test.go +++ /dev/null @@ -1,440 +0,0 @@ -package utils - -import ( - "context" - "io" - "net" - "net/http" - "os" - "testing" - - "github.com/go-errors/errors" - "github.com/h2non/gock" - "github.com/jackc/pgconn" - "github.com/jackc/pgx/v4" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/testing/apitest" - "github.com/supabase/cli/internal/utils/cloudflare" - "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/pgtest" -) - -var dbConfig = pgconn.Config{ - Host: GetSupabaseDbHost(apitest.RandomProjectRef()), - Port: 6543, - User: "admin", - Password: "password", - Database: "postgres", -} - -var ( - PG13_POOLER_URL = "postgres://postgres:[YOUR-PASSWORD]@aws-0-ap-southeast-1.pooler.supabase.com:6543/postgres?options=reference%3Dzupyfdrjfhbeevcogohz" - PG15_POOLER_URL = "postgres://postgres.zupyfdrjfhbeevcogohz:[YOUR-PASSWORD]@fly-0-sin.pooler.supabase.com:6543/postgres" -) - -func TestConnectByConfig(t *testing.T) { - t.Run("connects to remote postgres with DoH", func(t *testing.T) { - Config.Db.Pooler.ConnectionString = "" - DNSResolver.Value = DNS_OVER_HTTPS - // Setup http mock - defer gock.OffAll() - // pgx makes 2 calls to resolve ip for each connect request - gock.New("https://1.1.1.1"). - Get("/dns-query"). - MatchParam("name", dbConfig.Host). - MatchHeader("accept", "application/dns-json"). - Reply(http.StatusOK). - JSON(&cloudflare.DNSResponse{Answer: []cloudflare.DNSAnswer{ - {Type: cloudflare.TypeA, Data: "127.0.0.1"}, - }}) - gock.New("https://1.1.1.1"). - Get("/dns-query"). - MatchParam("name", dbConfig.Host). - MatchHeader("accept", "application/dns-json"). - Reply(http.StatusOK). - JSON(&cloudflare.DNSResponse{Answer: []cloudflare.DNSAnswer{ - {Type: cloudflare.TypeA, Data: "127.0.0.1"}, - }}) - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - // Run test - c, err := ConnectByConfig(context.Background(), dbConfig, conn.Intercept) - require.NoError(t, err) - defer c.Close(context.Background()) - assert.NoError(t, err) - }) - - t.Run("connects with unescaped db password", func(t *testing.T) { - DNSResolver.Value = DNS_GO_NATIVE - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - // Run test - config := *dbConfig.Copy() - config.Host = "localhost" - config.Password = "pass word" - c, err := ConnectByConfig(context.Background(), config, conn.Intercept) - require.NoError(t, err) - defer c.Close(context.Background()) - assert.Equal(t, config.Password, c.Config().Password) - }) - - t.Run("no retry on connecting successfully with pooler", func(t *testing.T) { - Config.Db.Pooler.ConnectionString = PG15_POOLER_URL - DNSResolver.Value = DNS_GO_NATIVE - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - // Run test - c, err := ConnectByConfig(context.Background(), dbConfig, conn.Intercept) - // Check error - require.NoError(t, err) - defer c.Close(context.Background()) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("fallback to postgres port on dial error", func(t *testing.T) { - Config.Db.Pooler.ConnectionString = PG15_POOLER_URL - DNSResolver.Value = DNS_OVER_HTTPS - netErr := errors.New("network error") - // Setup http mock - defer gock.OffAll() - gock.New("https://1.1.1.1"). - Get("/dns-query"). - MatchParam("name", dbConfig.Host). - MatchHeader("accept", "application/dns-json"). - ReplyError(&net.OpError{Op: "dial", Err: netErr}) - gock.New("https://1.1.1.1"). - Get("/dns-query"). - MatchParam("name", "fly-0-sin.pooler.supabase.com"). - MatchHeader("accept", "application/dns-json"). - ReplyError(&net.OpError{Op: "dial", Err: netErr}) - // Run test - _, err := ConnectByConfig(context.Background(), dbConfig) - // Check error - require.ErrorIs(t, err, netErr) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} - -func TestConnectLocal(t *testing.T) { - t.Run("connects with debug log", func(t *testing.T) { - viper.Set("DEBUG", true) - _, err := ConnectLocalPostgres(context.Background(), pgconn.Config{Host: "0", Port: 6543}) - assert.ErrorContains(t, err, "failed to connect to postgres") - }) - - t.Run("throws error on invalid port", func(t *testing.T) { - Config.Db.Port = 0 - _, err := ConnectLocalPostgres(context.Background(), pgconn.Config{}) - assert.ErrorContains(t, err, "invalid port (outside range)") - }) -} - -func TestPoolerConfig(t *testing.T) { - oldProfile := CurrentProfile - CurrentProfile = allProfiles[0] - defer t.Cleanup(func() { CurrentProfile = oldProfile }) - - t.Run("parses options ref", func(t *testing.T) { - Config.Db.Pooler.ConnectionString = PG13_POOLER_URL - assert.NotNil(t, GetPoolerConfig("zupyfdrjfhbeevcogohz")) - }) - - t.Run("parses username ref", func(t *testing.T) { - Config.Db.Pooler.ConnectionString = PG15_POOLER_URL - assert.NotNil(t, GetPoolerConfig("zupyfdrjfhbeevcogohz")) - }) - - t.Run("returns nil on missing url", func(t *testing.T) { - Config.Db.Pooler.ConnectionString = "" - assert.Nil(t, GetPoolerConfig("zupyfdrjfhbeevcogohz")) - }) - - t.Run("returns nil on malformed url", func(t *testing.T) { - Config.Db.Pooler.ConnectionString = "malformed" - assert.Nil(t, GetPoolerConfig("zupyfdrjfhbeevcogohz")) - }) - - t.Run("returns nil on mismatched project", func(t *testing.T) { - Config.Db.Pooler.ConnectionString = PG13_POOLER_URL - assert.Nil(t, GetPoolerConfig("nlhaskwsizylhnffaqkd")) - Config.Db.Pooler.ConnectionString = PG15_POOLER_URL - assert.Nil(t, GetPoolerConfig("nlhaskwsizylhnffaqkd")) - }) - - t.Run("returns nil on invalid host", func(t *testing.T) { - Config.Db.Pooler.ConnectionString = "postgres://postgres.zupyfdrjfhbeevcogohz:[YOUR-PASSWORD]@localhost:6543/postgres" - assert.Nil(t, GetPoolerConfig("zupyfdrjfhbeevcogohz")) - }) -} - -func TestSetConnectSuggestion(t *testing.T) { - oldProfile := CurrentProfile - CurrentProfile = allProfiles[0] - defer t.Cleanup(func() { CurrentProfile = oldProfile }) - - cases := []struct { - name string - err error - suggestion string - debug bool - }{ - { - name: "no-op on nil error", - err: nil, - suggestion: "", - }, - { - name: "no-op on unrecognised error", - err: errors.New("some unknown error"), - suggestion: "", - }, - { - name: "connection refused", - err: errors.New("connect: connection refused"), - suggestion: "Make sure your local IP is allowed in Network Restrictions and Network Bans", - }, - { - name: "address not in allow list", - err: errors.New("server error (FATAL: Address not in tenant allow_list: {1,2,3} (SQLSTATE XX000))"), - suggestion: "Make sure your local IP is allowed in Network Restrictions and Network Bans", - }, - { - name: "ssl required without debug flag", - err: errors.New("SSL connection is required"), - suggestion: "", - }, - { - // The debug proxy negotiates TLS itself, so --debug is no longer the - // reason a server rejects an unencrypted connection (#5872). - name: "ssl required with debug flag", - err: errors.New("SSL connection is required"), - debug: true, - suggestion: "", - }, - { - name: "wrong password via SCRAM", - err: errors.New("SCRAM exchange: Wrong password"), - suggestion: "Connect to your database by setting the env var correctly: SUPABASE_DB_PASSWORD", - }, - { - name: "failed SASL auth", - err: errors.New("failed SASL auth"), - suggestion: "Connect to your database by setting the env var correctly: SUPABASE_DB_PASSWORD", - }, - { - name: "ipv6 no route to host", - err: errors.New("dial tcp [2406:da18:4fd:9b0d:80ec:9812:3e65:450b]:5432: connect: no route to host"), - suggestion: "Your network does not support IPv6", - }, - { - name: "ipv6 network is unreachable", - err: errors.New("dial tcp [2406:da18:4fd:9b0d:80ec:9812:3e65:450b]:5432: connect: network is unreachable"), - suggestion: "Your network does not support IPv6", - }, - { - name: "libpq unsupported address family", - err: errors.New(`pg_dump: error: connection to server failed: could not translate host name "db.test.supabase.co" to address: Address family for hostname not supported`), - suggestion: "Your network does not support IPv6", - }, - { - name: "libpq no address associated with hostname", - err: errors.New(`pg_dump: error: could not translate host name "db.ngpopfcjxrfmzmhmmpct.supabase.co" to address: No address associated with hostname`), - suggestion: "Your network does not support IPv6", - }, - { - name: "libpq network is unreachable without literal", - err: errors.New(`connection to server at "db.test.supabase.co", port 5432 failed: Network is unreachable`), - suggestion: "Your network does not support IPv6", - }, - { - name: "libpq cannot assign requested address", - err: errors.New(`pg_dump: error: connection to server at "db.test.supabase.co" (2600:1f1c:c19:4901:963f:d22e:683a:381c), port 5432 failed: Cannot assign requested address - Is the server running on that host and accepting TCP/IP connections?`), - suggestion: "Your network does not support IPv6", - }, - { - name: "cannot assign requested address without ipv6 literal", - err: errors.New("connect: cannot assign requested address"), - suggestion: "", - }, - { - name: "no route to host without ipv6 address", - err: errors.New("connect: no route to host"), - suggestion: "Make sure your project exists on profile: " + CurrentProfile.Name, - }, - { - name: "tenant or user not found", - err: errors.New("Tenant or user not found"), - suggestion: "Make sure your project exists on profile: " + CurrentProfile.Name, - }, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - CmdSuggestion = "" - viper.Set("DEBUG", tc.debug) - SetConnectSuggestion(tc.err) - if tc.suggestion == "" { - assert.Empty(t, CmdSuggestion) - } else { - assert.Contains(t, CmdSuggestion, tc.suggestion) - } - }) - } -} - -func TestSuggestIPv6Pooler(t *testing.T) { - ref := apitest.RandomProjectRef() - poolerURL := "postgres://postgres." + ref + ":[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:6543/postgres" - - t.Run("enriches suggestion with transaction pooler url", func(t *testing.T) { - CmdSuggestion = "" - t.Cleanup(func() { CmdSuggestion = "" }) - t.Cleanup(apitest.MockPlatformAPI(t)) - gock.New(DefaultApiHost). - Get("/v1/projects/" + ref + "/config/database/pooler"). - Reply(http.StatusOK). - JSON([]api.SupavisorConfigResponseOutput{{ - DatabaseType: api.SupavisorConfigResponseOutputDatabaseTypePRIMARY, - ConnectionString: poolerURL, - }}) - ok := SuggestIPv6Pooler(context.Background(), "db."+ref+".supabase.co") - assert.True(t, ok) - assert.Contains(t, CmdSuggestion, "--db-url") - assert.Contains(t, CmdSuggestion, poolerURL) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("masks a real password returned by the api", func(t *testing.T) { - CmdSuggestion = "" - t.Cleanup(func() { CmdSuggestion = "" }) - t.Cleanup(apitest.MockPlatformAPI(t)) - secretURL := "postgres://postgres." + ref + ":sup3r-s3cret@aws-0-us-east-1.pooler.supabase.com:6543/postgres" - gock.New(DefaultApiHost). - Get("/v1/projects/" + ref + "/config/database/pooler"). - Reply(http.StatusOK). - JSON([]api.SupavisorConfigResponseOutput{{ - DatabaseType: api.SupavisorConfigResponseOutputDatabaseTypePRIMARY, - ConnectionString: secretURL, - }}) - ok := SuggestIPv6Pooler(context.Background(), "db."+ref+".supabase.co") - assert.True(t, ok) - assert.NotContains(t, CmdSuggestion, "sup3r-s3cret") - assert.Contains(t, CmdSuggestion, "[YOUR-PASSWORD]") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("skips non-supabase host without api call", func(t *testing.T) { - CmdSuggestion = "" - assert.False(t, SuggestIPv6Pooler(context.Background(), "localhost")) - assert.Empty(t, CmdSuggestion) - }) - - t.Run("returns false when pooler config is unavailable", func(t *testing.T) { - CmdSuggestion = "" - t.Cleanup(apitest.MockPlatformAPI(t)) - gock.New(DefaultApiHost). - Get("/v1/projects/" + ref + "/config/database/pooler"). - Reply(http.StatusOK). - JSON([]api.SupavisorConfigResponseOutput{}) - assert.False(t, SuggestIPv6Pooler(context.Background(), "db."+ref+".supabase.co")) - assert.Empty(t, CmdSuggestion) - }) -} - -func TestProjectRefFromDirectDbHost(t *testing.T) { - ref := apitest.RandomProjectRef() - - t.Run("extracts ref from direct host", func(t *testing.T) { - got, ok := ProjectRefFromDirectDbHost("db." + ref + ".supabase.co") - assert.True(t, ok) - assert.Equal(t, ref, got) - }) - - t.Run("rejects pooler and local hosts", func(t *testing.T) { - for _, host := range []string{ - "aws-0-us-east-1.pooler.supabase.com", - "localhost", - "127.0.0.1", - "db." + ref + ".supabase.net", - } { - _, ok := ProjectRefFromDirectDbHost(host) - assert.False(t, ok, host) - } - }) -} - -func TestWarnIPv6PoolerFallback(t *testing.T) { - oldStderr := os.Stderr - r, w, err := os.Pipe() - require.NoError(t, err) - os.Stderr = w - t.Cleanup(func() { os.Stderr = oldStderr }) - - WarnIPv6PoolerFallback("db.test.supabase.co") - require.NoError(t, w.Close()) - out, err := io.ReadAll(r) - require.NoError(t, err) - - assert.Contains(t, string(out), "db.test.supabase.co") - assert.Contains(t, string(out), "does not support IPv6") - assert.Contains(t, string(out), "connection pooler") -} - -func TestPostgresURL(t *testing.T) { - url := ToPostgresURL(pgconn.Config{ - Host: "2406:da18:4fd:9b0d:80ec:9812:3e65:450b", - Port: 5432, - User: "postgres", - Password: "!@#$%^&*()", - RuntimeParams: map[string]string{ - "options": "test", - }, - }) - assert.Equal(t, `postgresql://postgres:%21%40%23$%25%5E&%2A%28%29@[2406:da18:4fd:9b0d:80ec:9812:3e65:450b]:5432/?connect_timeout=10&options=test`, url) -} - -func TestPreserveTLSConfig(t *testing.T) { - const dsn = "postgresql://postgres:pw@example.com:5432/postgres" - - t.Run("replays the sslmode resolved from the connection string", func(t *testing.T) { - for _, sslmode := range []string{"disable", "require", "verify-full"} { - parsed, err := pgconn.ParseConfig(dsn + "?sslmode=" + sslmode) - require.NoError(t, err) - rebuilt, err := pgx.ParseConfig(ToPostgresURL(*parsed)) - require.NoError(t, err) - // Run test - preserveTLSConfig(*parsed)(rebuilt) - // Check TLS matches the DSN, not the re-parse default of "prefer" - assert.Equal(t, parsed.TLSConfig, rebuilt.TLSConfig, sslmode) - } - }) - - t.Run("mandates TLS even when PGSSLMODE would disable it", func(t *testing.T) { - t.Setenv("PGSSLMODE", "disable") - parsed, err := pgconn.ParseConfig(dsn + "?sslmode=require") - require.NoError(t, err) - require.NotNil(t, parsed.TLSConfig) - rebuilt, err := pgx.ParseConfig(ToPostgresURL(*parsed)) - require.NoError(t, err) - require.Nil(t, rebuilt.TLSConfig) - // Run test - preserveTLSConfig(*parsed)(rebuilt) - // Check the env var no longer overrides the connection string - assert.NotNil(t, rebuilt.TLSConfig) - }) - - t.Run("leaves configs assembled in code untouched", func(t *testing.T) { - rebuilt, err := pgx.ParseConfig(ToPostgresURL(dbConfig)) - require.NoError(t, err) - before := rebuilt.TLSConfig - // Run test - preserveTLSConfig(dbConfig)(rebuilt) - // Check libpq's default resolution is preserved - assert.Same(t, before, rebuilt.TLSConfig) - }) -} diff --git a/apps/cli-go/internal/utils/console.go b/apps/cli-go/internal/utils/console.go deleted file mode 100644 index 8a34b8ed0e..0000000000 --- a/apps/cli-go/internal/utils/console.go +++ /dev/null @@ -1,107 +0,0 @@ -package utils - -import ( - "bufio" - "context" - "fmt" - "os" - "strings" - "sync" - "time" - - "github.com/go-errors/errors" - "github.com/spf13/viper" - "github.com/supabase/cli/pkg/cast" - "golang.org/x/term" -) - -type Console struct { - IsTTY bool - stdin *bufio.Scanner - token chan string - mu sync.Mutex -} - -func NewConsole() *Console { - return &Console{ - IsTTY: term.IsTerminal(int(os.Stdin.Fd())), - stdin: bufio.NewScanner(os.Stdin), - token: make(chan string), - mu: sync.Mutex{}, - } -} - -// Prevent interactive terminals from hanging more than 10 minutes -const ttyTimeout = time.Minute * 10 -const nonTTYTimeout = time.Millisecond * 100 - -func (c *Console) ReadLine(ctx context.Context) string { - timeout := nonTTYTimeout - if c.IsTTY { - timeout = ttyTimeout - } - timer := time.NewTimer(timeout) - defer timer.Stop() - // Read from stdin in background - go func() { - c.mu.Lock() - defer c.mu.Unlock() - // Scan one line from input or file - if c.stdin.Scan() { - c.token <- strings.TrimSpace(c.stdin.Text()) - } - }() - var input string - select { - case input = <-c.token: - case <-ctx.Done(): - case <-timer.C: - } - return input -} - -// PromptYesNo asks yes/no questions using the label. -func (c *Console) PromptYesNo(ctx context.Context, label string, def bool) (bool, error) { - choices := "Y/n" - if !def { - choices = "y/N" - } - labelWithChoice := fmt.Sprintf("%s [%s] ", label, choices) - if viper.GetBool("YES") { - fmt.Fprintln(os.Stderr, labelWithChoice+"y") - return true, nil - } - // Any error will be handled as default value - input, err := c.PromptText(ctx, labelWithChoice) - if len(input) > 0 { - if answer := parseYesNo(input); answer != nil { - return *answer, nil - } - } - return def, err -} - -func parseYesNo(s string) *bool { - s = strings.ToLower(s) - if s == "y" || s == "yes" { - return cast.Ptr(true) - } - if s == "n" || s == "no" { - return cast.Ptr(false) - } - return nil -} - -// PromptText asks for input using the label. -func (c *Console) PromptText(ctx context.Context, label string) (string, error) { - fmt.Fprint(os.Stderr, label) - input := c.ReadLine(ctx) - // Echo to stderr for non-interactive terminals - if !c.IsTTY { - fmt.Fprintln(os.Stderr, input) - } - if err := ctx.Err(); err != nil { - return "", errors.New(err) - } - return input, nil -} diff --git a/apps/cli-go/internal/utils/console_test.go b/apps/cli-go/internal/utils/console_test.go deleted file mode 100644 index ffc9532e75..0000000000 --- a/apps/cli-go/internal/utils/console_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package utils - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/supabase/cli/internal/testing/fstest" -) - -func TestPromptYesNo(t *testing.T) { - t.Run("defaults on closed stdin", func(t *testing.T) { - c := NewConsole() - c.IsTTY = false - // Run test - val, err := c.PromptYesNo(context.Background(), "test", false) - // Check error - assert.NoError(t, err) - assert.False(t, val) - }) - - t.Run("parses piped stdin", func(t *testing.T) { - t.Cleanup(fstest.MockStdin(t, "y")) - c := NewConsole() - // Run test - val, err := c.PromptYesNo(context.Background(), "test", false) - // Check error - assert.NoError(t, err) - assert.True(t, val) - }) -} - -func TestPromptText(t *testing.T) { - t.Run("defaults on timeout", func(t *testing.T) { - t.Cleanup(fstest.MockStdin(t, "")) - c := NewConsole() - // Run test - val, err := c.PromptText(context.Background(), "test") - // Check error - assert.NoError(t, err) - assert.Empty(t, val) - }) - - t.Run("throws error on cancel", func(t *testing.T) { - c := NewConsole() - // Setup cancelled context - ctx, cancel := context.WithCancel(context.Background()) - cancel() - // Run test - val, err := c.PromptText(ctx, "test") - // Check error - assert.ErrorIs(t, err, context.Canceled) - assert.Empty(t, val) - }) -} diff --git a/apps/cli-go/internal/utils/container_output.go b/apps/cli-go/internal/utils/container_output.go deleted file mode 100644 index 912d071a41..0000000000 --- a/apps/cli-go/internal/utils/container_output.go +++ /dev/null @@ -1,53 +0,0 @@ -package utils - -import ( - "encoding/json" - "io" - "strings" - - "github.com/docker/docker/pkg/jsonmessage" -) - -func ProcessPullOutput(out io.ReadCloser, p Program) error { - dec := json.NewDecoder(out) - - downloads := make(map[string]struct{ current, total int64 }) - - for { - var progress jsonmessage.JSONMessage - - if err := dec.Decode(&progress); err == io.EOF { - break - } else if err != nil { - return err - } - - if strings.HasPrefix(progress.Status, "Pulling from") { - p.Send(StatusMsg(progress.Status + "...")) - } else if progress.Status == "Pulling fs layer" || progress.Status == "Waiting" { - downloads[progress.ID] = struct{ current, total int64 }{ - current: 0, - total: 0, - } - } else if progress.Status == "Downloading" { - downloads[progress.ID] = struct{ current, total int64 }{ - current: progress.Progress.Current, - total: progress.Progress.Total, - } - - var overallProgress float64 - for _, percentage := range downloads { - if percentage.total > 0 { - progress := float64(percentage.current) / float64(percentage.total) - overallProgress += progress / float64(len(downloads)) - } - } - - p.Send(ProgressMsg(&overallProgress)) - } - } - - p.Send(ProgressMsg(nil)) - - return nil -} diff --git a/apps/cli-go/internal/utils/container_output_test.go b/apps/cli-go/internal/utils/container_output_test.go deleted file mode 100644 index 44376e2d34..0000000000 --- a/apps/cli-go/internal/utils/container_output_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package utils - -import ( - "encoding/json" - "io" - "sync" - "testing" - - tea "github.com/charmbracelet/bubbletea" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/stretchr/testify/assert" -) - -func TestProcessPullOutput(t *testing.T) { - t.Run("processes docker pull messages", func(t *testing.T) { - messages := []jsonmessage.JSONMessage{ - {Status: "Pulling from library/postgres"}, - {ID: "layer1", Status: "Pulling fs layer"}, - {ID: "layer1", Status: "Downloading", Progress: &jsonmessage.JSONProgress{Current: 50, Total: 100}}, - {ID: "layer2", Status: "Pulling fs layer"}, - {ID: "layer2", Status: "Downloading", Progress: &jsonmessage.JSONProgress{Current: 75, Total: 100}}, - } - - // Create a pipe to write messages - r, w := io.Pipe() - enc := json.NewEncoder(w) - go func() { - for _, msg := range messages { - err := enc.Encode(msg) - assert.Nil(t, err) - } - w.Close() - }() - - var status string - var progressFirst *float64 - var progress *float64 - p := NewMockProgram(func(msg tea.Msg) { - switch m := msg.(type) { - case StatusMsg: - status = string(m) - case ProgressMsg: - progress = m - if progressFirst == nil { - progressFirst = m - } - } - }) - - err := ProcessPullOutput(r, p) - - assert.NoError(t, err) - assert.Equal(t, "Pulling from library/postgres...", status) - assert.Equal(t, *progressFirst, 0.5) - assert.Nil(t, progress) - }) -} - -type MockProgram struct { - handler func(tea.Msg) - mu sync.Mutex // Add mutex for thread safety -} - -func NewMockProgram(handler func(tea.Msg)) *MockProgram { - return &MockProgram{ - handler: handler, - } -} - -func (m *MockProgram) Send(msg tea.Msg) { - if m.handler != nil { - m.mu.Lock() - m.handler(msg) - m.mu.Unlock() - } -} - -func (m *MockProgram) Start() error { - return nil -} - -func (m *MockProgram) Quit() {} diff --git a/apps/cli-go/internal/utils/credentials/input.go b/apps/cli-go/internal/utils/credentials/input.go deleted file mode 100644 index 290b807aea..0000000000 --- a/apps/cli-go/internal/utils/credentials/input.go +++ /dev/null @@ -1,30 +0,0 @@ -package credentials - -import ( - "bytes" - "fmt" - "io" - "os" - - "golang.org/x/term" -) - -func PromptMasked(stdin *os.File) string { - // Start a new line after reading input - defer fmt.Println() - // Copy if stdin is piped: https://stackoverflow.com/a/26567513 - if !term.IsTerminal(int(stdin.Fd())) { - var buf bytes.Buffer - if _, err := io.Copy(&buf, stdin); err != nil { - return "" - } - return buf.String() - } - // Read with masked tokens - bytepw, err := term.ReadPassword(int(stdin.Fd())) - if err != nil { - fmt.Fprintln(os.Stderr, "Failed to read password:", err) - return "" - } - return string(bytepw) -} diff --git a/apps/cli-go/internal/utils/credentials/input_test.go b/apps/cli-go/internal/utils/credentials/input_test.go deleted file mode 100644 index c83d6d3adc..0000000000 --- a/apps/cli-go/internal/utils/credentials/input_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package credentials - -import ( - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestPromptMasked(t *testing.T) { - t.Run("reads from piped stdin", func(t *testing.T) { - // Setup token - r, w, err := os.Pipe() - require.NoError(t, err) - _, err = w.WriteString("token") - require.NoError(t, err) - require.NoError(t, w.Close()) - // Run test - input := PromptMasked(r) - // Check error - assert.Equal(t, "token", input) - }) - - t.Run("empty string on closed pipe", func(t *testing.T) { - // Setup empty stdin - r, _, err := os.Pipe() - require.NoError(t, err) - require.NoError(t, r.Close()) - // Run test - input := PromptMasked(r) - // Check error - assert.Empty(t, input) - }) -} diff --git a/apps/cli-go/internal/utils/credentials/keyring_test.go b/apps/cli-go/internal/utils/credentials/keyring_test.go deleted file mode 100644 index 4f450078f2..0000000000 --- a/apps/cli-go/internal/utils/credentials/keyring_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package credentials - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/zalando/go-keyring" -) - -func TestDeleteAll(t *testing.T) { - service := "test-cli" - // Nothing to delete - err := keyring.DeleteAll(service) - assert.NoError(t, err) - // Setup 2 items - err = keyring.Set(service, "key1", "value") - assert.NoError(t, err) - err = keyring.Set(service, "key2", "value") - assert.NoError(t, err) - // Delete all items - err = keyring.DeleteAll(service) - assert.NoError(t, err) - // Check items are gone - _, err = keyring.Get(service, "key1") - assert.ErrorIs(t, err, keyring.ErrNotFound) - _, err = keyring.Get(service, "key2") - assert.ErrorIs(t, err, keyring.ErrNotFound) -} diff --git a/apps/cli-go/internal/utils/credentials/store.go b/apps/cli-go/internal/utils/credentials/store.go deleted file mode 100644 index d7bf9d2b2d..0000000000 --- a/apps/cli-go/internal/utils/credentials/store.go +++ /dev/null @@ -1,88 +0,0 @@ -package credentials - -import ( - "bytes" - "os" - "os/exec" - - "github.com/go-errors/errors" - "github.com/zalando/go-keyring" -) - -const namespace = "Supabase CLI" - -var ErrNotSupported = errors.New("Keyring is not supported on WSL") - -type Store interface { - Get(key string) (string, error) - Set(key, value string) error - Delete(project string) error - DeleteAll() error -} - -type KeyringStore struct{} - -var StoreProvider Store = &KeyringStore{} - -// Get retrieves the password for a project from the keyring. -func (ks *KeyringStore) Get(project string) (string, error) { - if err := assertKeyringSupported(); err != nil { - return "", err - } - val, err := keyring.Get(namespace, project) - if errors.Is(err, exec.ErrNotFound) { - return "", errors.New(ErrNotSupported) - } else if err != nil { - return "", errors.Errorf("failed to load credentials: %w", err) - } - return val, nil -} - -func (ks *KeyringStore) Set(project, password string) error { - if err := assertKeyringSupported(); err != nil { - return err - } - if err := keyring.Set(namespace, project, password); err != nil { - if errors.Is(err, exec.ErrNotFound) { - return ErrNotSupported - } - return errors.Errorf("failed to set credentials: %w", err) - } - return nil -} - -func (ks *KeyringStore) Delete(project string) error { - if err := assertKeyringSupported(); err != nil { - return err - } - if err := keyring.Delete(namespace, project); err != nil { - if errors.Is(err, exec.ErrNotFound) { - return ErrNotSupported - } - return errors.Errorf("failed to delete credentials: %w", err) - } - return nil -} - -func (ks *KeyringStore) DeleteAll() error { - if err := assertKeyringSupported(); err != nil { - return err - } - if err := keyring.DeleteAll(namespace); err != nil { - if errors.Is(err, exec.ErrNotFound) { - return ErrNotSupported - } - return errors.Errorf("failed to delete all credentials in %s: %w", namespace, err) - } - return nil -} - -func assertKeyringSupported() error { - // Suggested check: https://github.com/microsoft/WSL/issues/423 - if f, err := os.ReadFile("/proc/sys/kernel/osrelease"); err == nil { - if bytes.Contains(f, []byte("WSL")) || bytes.Contains(f, []byte("Microsoft")) { - return errors.New(ErrNotSupported) - } - } - return nil -} diff --git a/apps/cli-go/internal/utils/credentials/store_test.go b/apps/cli-go/internal/utils/credentials/store_test.go deleted file mode 100644 index 4bb62f6581..0000000000 --- a/apps/cli-go/internal/utils/credentials/store_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package credentials - -import ( - "testing" - - "github.com/go-errors/errors" - "github.com/stretchr/testify/assert" - "github.com/zalando/go-keyring" -) - -func TestKeyringStore(t *testing.T) { - t.Run("stores and retrieves password", func(t *testing.T) { - keyring.MockInit() - project := "test-project" - password := "test-password" - - err := StoreProvider.Set(project, password) - assert.NoError(t, err) - - retrieved, err := StoreProvider.Get(project) - assert.NoError(t, err) - assert.Equal(t, password, retrieved) - }) - - t.Run("returns error for non-existent project", func(t *testing.T) { - keyring.MockInit() - project := "non-existent" - - retrieved, err := StoreProvider.Get(project) - assert.ErrorIs(t, err, keyring.ErrNotFound) - assert.Empty(t, retrieved) - }) - - t.Run("deletes specific project password", func(t *testing.T) { - keyring.MockInit() - project := "test-project" - password := "test-password" - - err := StoreProvider.Set(project, password) - assert.NoError(t, err) - err = StoreProvider.Delete(project) - assert.NoError(t, err) - - _, err = StoreProvider.Get(project) - assert.ErrorIs(t, err, keyring.ErrNotFound) - }) - - t.Run("deletes all project passwords", func(t *testing.T) { - keyring.MockInit() - projects := []string{"project1", "project2"} - - for _, project := range projects { - err := StoreProvider.Set(project, "password") - assert.NoError(t, err) - } - - err := StoreProvider.DeleteAll() - assert.NoError(t, err) - - for _, project := range projects { - _, err := StoreProvider.Get(project) - assert.ErrorIs(t, err, keyring.ErrNotFound) - } - }) -} - -func TestKeyringErrors(t *testing.T) { - t.Run("handles Get error", func(t *testing.T) { - mockErr := errors.New("mock error") - keyring.MockInitWithError(mockErr) - - _, err := StoreProvider.Get("test") - assert.ErrorIs(t, err, mockErr) - }) - - t.Run("handles Set error", func(t *testing.T) { - mockErr := errors.New("mock error") - keyring.MockInitWithError(mockErr) - - err := StoreProvider.Set("test", "pass") - assert.ErrorIs(t, err, mockErr) - }) - - t.Run("handles Delete error", func(t *testing.T) { - mockErr := errors.New("mock error") - keyring.MockInitWithError(mockErr) - - err := StoreProvider.Delete("test") - assert.ErrorIs(t, err, mockErr) - }) - - t.Run("handles DeleteAll error", func(t *testing.T) { - mockErr := errors.New("mock error") - keyring.MockInitWithError(mockErr) - - err := StoreProvider.DeleteAll() - assert.ErrorIs(t, err, mockErr) - }) -} diff --git a/apps/cli-go/internal/utils/deno.go b/apps/cli-go/internal/utils/deno.go deleted file mode 100644 index 79786c0b56..0000000000 --- a/apps/cli-go/internal/utils/deno.go +++ /dev/null @@ -1,272 +0,0 @@ -package utils - -import ( - "archive/zip" - "bytes" - "context" - "crypto/sha256" - "embed" - "fmt" - "io" - "io/fs" - "net/http" - "os" - "os/exec" - "path" - "path/filepath" - "runtime" - "strings" - - "github.com/go-errors/errors" - "github.com/spf13/afero" - "github.com/supabase/cli/pkg/function" -) - -var ( - //go:embed denos/* - denoEmbedDir embed.FS - // Used by unit tests - DenoPathOverride string -) - -const ( - // Legacy bundle options - DockerDenoDir = "/home/deno" - DockerEszipDir = "/root/eszips" - DenoVersion = "1.30.3" -) - -func GetDenoPath() (string, error) { - if len(DenoPathOverride) > 0 { - return DenoPathOverride, nil - } - home, err := SupabaseHomeDir() - if err != nil { - return "", err - } - denoBinName := "deno" - if runtime.GOOS == "windows" { - denoBinName = "deno.exe" - } - denoPath := filepath.Join(home, denoBinName) - return denoPath, nil -} - -func InstallOrUpgradeDeno(ctx context.Context, fsys afero.Fs) error { - denoPath, err := GetDenoPath() - if err != nil { - return err - } - - if _, err := fsys.Stat(denoPath); err == nil { - // Upgrade Deno. - cmd := exec.CommandContext(ctx, denoPath, "upgrade", "--version", DenoVersion) - cmd.Stderr = os.Stderr - cmd.Stdout = os.Stdout - return cmd.Run() - } else if !errors.Is(err, os.ErrNotExist) { - return err - } - - // Install Deno. - if err := MkdirIfNotExistFS(fsys, filepath.Dir(denoPath)); err != nil { - return err - } - - // 1. Determine OS triple - assetFilename, err := getDenoAssetFileName() - if err != nil { - return err - } - assetRepo := "denoland/deno" - if runtime.GOOS == "linux" && runtime.GOARCH == "arm64" { - // TODO: version pin to official release once available https://github.com/denoland/deno/issues/1846 - assetRepo = "LukeChannings/deno-arm64" - } - - // 2. Download & install Deno binary. - { - assetUrl := fmt.Sprintf("https://github.com/%s/releases/download/v%s/%s", assetRepo, DenoVersion, assetFilename) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, assetUrl, nil) - if err != nil { - return err - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return errors.New("Failed installing Deno binary.") - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - - r, err := zip.NewReader(bytes.NewReader(body), int64(len(body))) - // There should be only 1 file: the deno binary - if len(r.File) != 1 { - return err - } - denoContents, err := r.File[0].Open() - if err != nil { - return err - } - defer denoContents.Close() - - denoBytes, err := io.ReadAll(denoContents) - if err != nil { - return err - } - - if err := afero.WriteFile(fsys, denoPath, denoBytes, 0755); err != nil { - return err - } - } - - return nil -} - -func isScriptModified(fsys afero.Fs, destPath string, src []byte) (bool, error) { - dest, err := afero.ReadFile(fsys, destPath) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - return true, nil - } - return false, err - } - - // compare the md5 checksum of src bytes with user's copy. - // if the checksums doesn't match, script is modified. - return sha256.Sum256(dest) != sha256.Sum256(src), nil -} - -type DenoScriptDir struct { - ExtractPath string - BuildPath string -} - -// Copy Deno scripts needed for function deploy and downloads, returning a DenoScriptDir struct or an error. -func CopyDenoScripts(ctx context.Context, fsys afero.Fs) (*DenoScriptDir, error) { - denoPath, err := GetDenoPath() - if err != nil { - return nil, err - } - - denoDirPath := filepath.Dir(denoPath) - scriptDirPath := filepath.Join(denoDirPath, "denos") - - // make the script directory if not exist - if err := MkdirIfNotExistFS(fsys, scriptDirPath); err != nil { - return nil, err - } - - // copy embed files to script directory - err = fs.WalkDir(denoEmbedDir, "denos", func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - - // skip copying the directory - if d.IsDir() { - return nil - } - - destPath := filepath.Join(denoDirPath, path) - - contents, err := fs.ReadFile(denoEmbedDir, path) - if err != nil { - return err - } - - // check if the script should be copied - modified, err := isScriptModified(fsys, destPath, contents) - if err != nil { - return err - } - if !modified { - return nil - } - - if err := afero.WriteFile(fsys, filepath.Join(denoDirPath, path), contents, 0666); err != nil { - return err - } - - return nil - }) - if err != nil { - return nil, err - } - - sd := DenoScriptDir{ - ExtractPath: filepath.Join(scriptDirPath, "extract.ts"), - BuildPath: filepath.Join(scriptDirPath, "build.ts"), - } - - return &sd, nil -} - -func BindHostModules(cwd, relEntrypointPath, relImportMapPath string, fsys afero.Fs) ([]string, error) { - var modules []string - bindModule := func(srcPath string, r io.Reader) error { - hostPath := filepath.Join(cwd, filepath.FromSlash(srcPath)) - dockerPath := ToDockerPath(hostPath) - modules = append(modules, hostPath+":"+dockerPath+":ro") - return nil - } - importMap := function.ImportMap{} - if imPath := filepath.ToSlash(relImportMapPath); len(imPath) > 0 { - if err := importMap.LoadAsDeno(imPath, afero.NewIOFS(fsys), bindModule); err != nil { - return nil, err - } - } - // Resolving all Import Graph - addModule := func(unixPath string, w io.Writer) error { - hostPath := toHostPath(cwd, unixPath) - f, err := fsys.Open(hostPath) - if err != nil { - return errors.Errorf("failed to read file: %w", err) - } - defer f.Close() - if _, err := io.Copy(w, f); err != nil { - return errors.Errorf("failed to copy file content: %w", err) - } - dockerPath := ToDockerPath(hostPath) - modules = append(modules, hostPath+":"+dockerPath+":ro") - return nil - } - unixPath := filepath.ToSlash(relEntrypointPath) - if err := importMap.WalkImportPaths(unixPath, addModule); err != nil { - return nil, err - } - // Also mount local directories declared in scopes - for _, scope := range importMap.Scopes { - for _, unixPath := range scope { - hostPath := toHostPath(cwd, unixPath) - // Ref: https://docs.deno.com/runtime/fundamentals/modules/#overriding-https-imports - if _, err := fsys.Stat(hostPath); err != nil { - return nil, errors.Errorf("failed to resolve scope: %w", err) - } - dockerPath := ToDockerPath(hostPath) - modules = append(modules, hostPath+":"+dockerPath+":ro") - } - } - return modules, nil -} - -func toHostPath(cwd, unixPath string) string { - hostPath := filepath.FromSlash(unixPath) - if path.IsAbs(unixPath) { - return filepath.VolumeName(cwd) + hostPath - } - return filepath.Join(cwd, hostPath) -} - -func ToDockerPath(absHostPath string) string { - prefix := filepath.VolumeName(absHostPath) - dockerPath := filepath.ToSlash(absHostPath) - return strings.TrimPrefix(dockerPath, prefix) -} diff --git a/apps/cli-go/internal/utils/deno_darwin.go b/apps/cli-go/internal/utils/deno_darwin.go deleted file mode 100644 index fe0b89d10e..0000000000 --- a/apps/cli-go/internal/utils/deno_darwin.go +++ /dev/null @@ -1,29 +0,0 @@ -//go:build darwin - -package utils - -import ( - "syscall" - - "github.com/go-errors/errors" -) - -func getDenoAssetFileName() (string, error) { - // Simple runtime.GOARCH detection doesn't work if the CLI is - // running under Rosetta: - // https://github.com/supabase/cli/issues/1266. So as a workaround - // we use Apple Silicon detection: - // https://www.yellowduck.be/posts/detecting-apple-silicon-via-go. - _, err := syscall.Sysctl("sysctl.proc_translated") - if err != nil { - if err.Error() == "no such file or directory" { - // Running on Intel Mac. - return "deno-x86_64-apple-darwin.zip", nil - } else { - return "", errors.Errorf("failed to determine OS triple: %w", err) - } - } else { - // Running on Apple Silicon. - return "deno-aarch64-apple-darwin.zip", nil - } -} diff --git a/apps/cli-go/internal/utils/deno_generic.go b/apps/cli-go/internal/utils/deno_generic.go deleted file mode 100644 index 2992c7baba..0000000000 --- a/apps/cli-go/internal/utils/deno_generic.go +++ /dev/null @@ -1,22 +0,0 @@ -//go:build !darwin - -package utils - -import ( - "runtime" - - "github.com/go-errors/errors" -) - -func getDenoAssetFileName() (string, error) { - if runtime.GOOS == "linux" && runtime.GOARCH == "amd64" { - return "deno-x86_64-unknown-linux-gnu.zip", nil - } else if runtime.GOOS == "linux" && runtime.GOARCH == "arm64" { - // TODO: version pin to official release once available https://github.com/denoland/deno/issues/1846 - return "deno-linux-arm64.zip", nil - } else if runtime.GOOS == "windows" && runtime.GOARCH == "amd64" { - return "deno-x86_64-pc-windows-msvc.zip", nil - } else { - return "", errors.New("Platform " + runtime.GOOS + "/" + runtime.GOARCH + " is currently unsupported for Functions.") - } -} diff --git a/apps/cli-go/internal/utils/deno_test.go b/apps/cli-go/internal/utils/deno_test.go deleted file mode 100644 index 1c104870d7..0000000000 --- a/apps/cli-go/internal/utils/deno_test.go +++ /dev/null @@ -1,160 +0,0 @@ -package utils - -import ( - "context" - "os" - "path/filepath" - "runtime" - "testing" - - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestBindModules(t *testing.T) { - t.Run("binds docker imports", func(t *testing.T) { - fsys := afero.NewMemMapFs() - entrypoint := `import "https://deno.land" -import "/tmp/index.ts" -import "../common/index.ts" -import "../../../supabase/tests/index.ts" -import "./child/index.ts"` - require.NoError(t, WriteFile("/app/supabase/functions/hello/index.ts", []byte(entrypoint), fsys)) - require.NoError(t, WriteFile("/tmp/index.ts", []byte{}, fsys)) - require.NoError(t, WriteFile("/app/supabase/functions/common/index.ts", []byte{}, fsys)) - require.NoError(t, WriteFile("/app/supabase/tests/index.ts", []byte{}, fsys)) - require.NoError(t, WriteFile("/app/supabase/functions/hello/child/index.ts", []byte{}, fsys)) - // Run test - mods, err := BindHostModules("/app", "supabase/functions/hello/index.ts", "", fsys) - // Check error - assert.NoError(t, err) - assert.ElementsMatch(t, mods, []string{ - "/app/supabase/functions/hello/index.ts:/app/supabase/functions/hello/index.ts:ro", - "/tmp/index.ts:/tmp/index.ts:ro", - "/app/supabase/functions/common/index.ts:/app/supabase/functions/common/index.ts:ro", - "/app/supabase/tests/index.ts:/app/supabase/tests/index.ts:ro", - "/app/supabase/functions/hello/child/index.ts:/app/supabase/functions/hello/child/index.ts:ro", - }) - }) -} - -func TestGetDenoPath(t *testing.T) { - t.Run("returns override path when set", func(t *testing.T) { - override := "/custom/path/to/deno" - DenoPathOverride = override - defer func() { DenoPathOverride = "" }() - - path, err := GetDenoPath() - - assert.NoError(t, err) - assert.Equal(t, override, path) - }) - - t.Run("returns default path", func(t *testing.T) { - home, err := os.UserHomeDir() - require.NoError(t, err) - expected := filepath.Join(home, ".supabase", "deno") - if runtime.GOOS == "windows" { - expected += ".exe" - } - - path, err := GetDenoPath() - - assert.NoError(t, err) - assert.Equal(t, expected, path) - }) - - t.Run("returns SUPABASE_HOME path when configured", func(t *testing.T) { - t.Setenv("SUPABASE_HOME", "/custom/supabase") - expected := filepath.Join("/custom/supabase", "deno") - if runtime.GOOS == "windows" { - expected += ".exe" - } - - path, err := GetDenoPath() - - assert.NoError(t, err) - assert.Equal(t, expected, path) - }) -} - -func TestIsScriptModified(t *testing.T) { - t.Run("detects modified script", func(t *testing.T) { - fsys := afero.NewMemMapFs() - destPath := "/path/to/script.ts" - original := []byte("original content") - modified := []byte("modified content") - require.NoError(t, afero.WriteFile(fsys, destPath, modified, 0644)) - - isModified, err := isScriptModified(fsys, destPath, original) - - assert.NoError(t, err) - assert.True(t, isModified) - }) - - t.Run("detects unmodified script", func(t *testing.T) { - fsys := afero.NewMemMapFs() - destPath := "/path/to/script.ts" - content := []byte("test content") - require.NoError(t, afero.WriteFile(fsys, destPath, content, 0644)) - - isModified, err := isScriptModified(fsys, destPath, content) - - assert.NoError(t, err) - assert.False(t, isModified) - }) - - t.Run("handles non-existent script", func(t *testing.T) { - fsys := afero.NewMemMapFs() - destPath := "/path/to/script.ts" - content := []byte("test content") - - isModified, err := isScriptModified(fsys, destPath, content) - - assert.NoError(t, err) - assert.True(t, isModified) - }) -} - -func TestCopyDenoScripts(t *testing.T) { - t.Run("copies deno scripts", func(t *testing.T) { - fsys := afero.NewMemMapFs() - home, err := os.UserHomeDir() - require.NoError(t, err) - denoDir := filepath.Join(home, ".supabase") - require.NoError(t, fsys.MkdirAll(denoDir, 0755)) - - scripts, err := CopyDenoScripts(context.Background(), fsys) - - assert.NoError(t, err) - assert.NotNil(t, scripts) - extractExists, err := afero.Exists(fsys, scripts.ExtractPath) - assert.NoError(t, err) - assert.True(t, extractExists) - }) - - t.Run("skips copying unmodified scripts", func(t *testing.T) { - fsys := afero.NewMemMapFs() - home, err := os.UserHomeDir() - require.NoError(t, err) - scriptDir := filepath.Join(home, ".supabase", "denos") - require.NoError(t, fsys.MkdirAll(scriptDir, 0755)) - - scripts1, err := CopyDenoScripts(context.Background(), fsys) - require.NoError(t, err) - stat1, err := fsys.Stat(scripts1.ExtractPath) - require.NoError(t, err) - modTime1 := stat1.ModTime() - - // Second copy - scripts2, err := CopyDenoScripts(context.Background(), fsys) - require.NoError(t, err) - stat2, err := fsys.Stat(scripts2.ExtractPath) - require.NoError(t, err) - modTime2 := stat2.ModTime() - - // Verify file wasn't rewritten - assert.Equal(t, modTime1, modTime2) - }) -} diff --git a/apps/cli-go/internal/utils/denos/extract.ts b/apps/cli-go/internal/utils/denos/extract.ts deleted file mode 100644 index 1e47399677..0000000000 --- a/apps/cli-go/internal/utils/denos/extract.ts +++ /dev/null @@ -1,71 +0,0 @@ -import * as path from "https://deno.land/std@0.127.0/path/mod.ts"; -import { readAll } from "https://deno.land/std@0.162.0/streams/conversion.ts"; -import { Parser } from "https://deno.land/x/eszip@v0.30.0/mod.ts"; - -async function write(p: string, content: string) { - await Deno.mkdir(path.dirname(p), { recursive: true }); - await Deno.writeTextFile(p, content); -} - -async function loadEszip(bytes: Uint8Array) { - const parser = await Parser.createInstance(); - const specifiers = await parser.parseBytes(bytes); - await parser.load(); - return { parser, specifiers }; -} - -async function extractEszip( - destPath: string, - entrypointUrl: string, - rootPath: string, - parser: Parser, - specifiers: string[], -) { - const entrypointPath = path.fromFileUrl(entrypointUrl); - const basePath = path.dirname(entrypointPath); - // Containment is enforced against rootPath (the shared functions - // directory), not destPath (this function's own directory): a common - // layout imports a sibling like "../_shared/cors.ts", which legitimately - // resolves one level above destPath, but must never resolve outside - // rootPath entirely. - const resolvedRoot = path.resolve(rootPath); - for (const specifier of specifiers) { - // skip remote dependencies - if (!specifier.startsWith("file:")) { - continue; - } - console.error(specifier); - const module = await parser.getModuleSource(specifier); - const absPath = path.fromFileUrl(specifier); - const relPath = path.relative(basePath, absPath); - const dest = path.join(destPath, relPath); - - const resolvedDest = path.resolve(dest); - const relToRoot = path.relative(resolvedRoot, resolvedDest); - if ( - relToRoot === ".." || - relToRoot.startsWith(`..${path.SEP}`) || - path.isAbsolute(relToRoot) - ) { - throw new Error( - `Refusing to extract "${specifier}" outside of ${rootPath}`, - ); - } - - console.info(path.resolve(dest)); - await write(dest, module); - } -} - -async function extractSource( - destPath: string, - entrypointUrl: string, - rootPath: string, -) { - const buf = await readAll(Deno.stdin); - - const { parser, specifiers } = await loadEszip(buf); - await extractEszip(destPath, entrypointUrl, rootPath, parser, specifiers); -} - -extractSource(...Deno.args); diff --git a/apps/cli-go/internal/utils/docker.go b/apps/cli-go/internal/utils/docker.go deleted file mode 100644 index 29d2edd397..0000000000 --- a/apps/cli-go/internal/utils/docker.go +++ /dev/null @@ -1,686 +0,0 @@ -package utils - -import ( - "bytes" - "context" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "log" - "os" - "regexp" - "strings" - "sync" - "time" - - "github.com/containerd/errdefs" - podman "github.com/containers/common/libnetwork/types" - "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/compose/loader" - dockerConfig "github.com/docker/cli/cli/config" - dockerFlags "github.com/docker/cli/cli/flags" - "github.com/docker/cli/cli/streams" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/versions" - "github.com/docker/docker/api/types/volume" - "github.com/docker/docker/client" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/pkg/stdcopy" - "github.com/go-errors/errors" - "github.com/spf13/viper" - "go.opentelemetry.io/otel" -) - -var Docker = NewDocker() - -func NewDocker() *client.Client { - // TODO: refactor to initialize lazily - cli, err := command.NewDockerCli() - if err != nil { - log.Fatalln("Failed to create Docker client:", err) - } - // Silence otel errors as users don't care about docker metrics - // 2024/08/12 23:11:12 1 errors occurred detecting resource: - // * conflicting Schema URL: https://opentelemetry.io/schemas/1.21.0 - otel.SetErrorHandler(otel.ErrorHandlerFunc(func(cause error) {})) - if err := cli.Initialize(&dockerFlags.ClientOptions{}); err != nil { - log.Fatalln("Failed to initialize Docker client:", err) - } - return cli.Client().(*client.Client) -} - -const ( - DinDHost = "host.docker.internal" - CliProjectLabel = "com.supabase.cli.project" - composeProjectLabel = "com.docker.compose.project" -) - -func DockerNetworkCreateIfNotExists(ctx context.Context, mode container.NetworkMode, labels map[string]string) error { - // Non-user defined networks should already exist - if !isUserDefined(mode) { - return nil - } - _, err := Docker.NetworkCreate(ctx, mode.NetworkName(), network.CreateOptions{Labels: labels}) - // if error is network already exists, no need to propagate to user - if errdefs.IsConflict(err) || errors.Is(err, podman.ErrNetworkExists) { - return nil - } - if err != nil { - return errors.Errorf("failed to create docker network: %w", err) - } - return err -} - -func WaitAll[T any](containers []T, exec func(container T) error) []error { - var wg sync.WaitGroup - result := make([]error, len(containers)) - for i, container := range containers { - wg.Add(1) - go func(i int, container T) { - defer wg.Done() - result[i] = exec(container) - }(i, container) - } - wg.Wait() - return result -} - -// NoBackupVolume TODO: encapsulate this state in a class -var NoBackupVolume = false - -func DockerRemoveAll(ctx context.Context, w io.Writer, projectId string) error { - fmt.Fprintln(w, "Stopping containers...") - args := CliProjectFilter(projectId) - containers, err := Docker.ContainerList(ctx, container.ListOptions{ - All: true, - Filters: args, - }) - if err != nil { - return errors.Errorf("failed to list containers: %w", err) - } - // Gracefully shutdown containers - var ids []string - for _, c := range containers { - ids = append(ids, c.ID) - } - result := WaitAll(ids, func(id string) error { - if err := Docker.ContainerStop(ctx, id, container.StopOptions{}); err != nil && !errdefs.IsNotModified(err) { - return errors.Errorf("failed to stop container: %w", err) - } - return nil - }) - if err := errors.Join(result...); err != nil { - return err - } - if report, err := Docker.ContainersPrune(ctx, args); err != nil { - return errors.Errorf("failed to prune containers: %w", err) - } else if viper.GetBool("DEBUG") { - fmt.Fprintln(os.Stderr, "Pruned containers:", report.ContainersDeleted) - } - // Remove named volumes - if NoBackupVolume { - vargs := args.Clone() - if versions.GreaterThanOrEqualTo(Docker.ClientVersion(), "1.42") { - // Since docker engine 25.0.3, all flag is required to include named volumes. - // https://github.com/docker/cli/blob/master/cli/command/volume/prune.go#L76 - vargs.Add("all", "true") - } - if report, err := Docker.VolumesPrune(ctx, vargs); err != nil { - return errors.Errorf("failed to prune volumes: %w", err) - } else if viper.GetBool("DEBUG") { - fmt.Fprintln(os.Stderr, "Pruned volumes:", report.VolumesDeleted) - } - } - // Remove networks. - if report, err := Docker.NetworksPrune(ctx, args); err != nil { - return errors.Errorf("failed to prune networks: %w", err) - } else if viper.GetBool("DEBUG") { - fmt.Fprintln(os.Stderr, "Pruned network:", report.NetworksDeleted) - } - return nil -} - -func CliProjectFilter(projectId string) filters.Args { - if len(projectId) == 0 { - return filters.NewArgs( - filters.Arg("label", CliProjectLabel), - ) - } - return filters.NewArgs( - filters.Arg("label", CliProjectLabel+"="+projectId), - ) -} - -var ( - registryAuth sync.Map -) - -// registryAuthEntry memoises a single registry's encoded auth so that -// loadRegistryAuth runs at most once per registry, even when concurrent image -// pulls request the same registry simultaneously. -type registryAuthEntry struct { - once sync.Once - value string -} - -func GetRegistryAuth() string { - return getRegistryAuth(GetRegistry()) -} - -func GetRegistryAuthForImage(imageTag string) string { - return getRegistryAuth(registryFromImage(imageTag)) -} - -func getRegistryAuth(registry string) string { - // Docker stores Hub credentials under the legacy index server, which - // resolves to the `index.docker.io` hostname; a bare `docker.io` lookup - // never matches them. Normalising here also routes images Docker treats as - // Hub references (a registry override or repository segment without a dot, - // colon, or `localhost`) to the right credentials. - if registry == "docker.io" { - registry = "index.docker.io" - } - // LoadOrStore + a per-key sync.Once guarantees loadRegistryAuth runs exactly - // once per registry, even under the concurrent image pulls `supabase start` - // issues. The result (including the empty string for a missing or unreadable - // credential entry) is then reused, so the Docker config file is read once - // and the credential warning is printed at most once per registry. - cached, _ := registryAuth.LoadOrStore(registry, ®istryAuthEntry{}) - entry := cached.(*registryAuthEntry) - entry.once.Do(func() { - entry.value = loadRegistryAuth(registry) - }) - return entry.value -} - -func loadRegistryAuth(registry string) string { - config := dockerConfig.LoadDefaultConfigFile(os.Stderr) - // Ref: https://docs.docker.com/engine/api/sdk/examples/#pull-an-image-with-authentication - auth, err := config.GetAuthConfig(registry) - if err != nil { - fmt.Fprintln(os.Stderr, "Failed to load registry credentials:", err) - return "" - } - encoded, err := json.Marshal(auth) - if err != nil { - fmt.Fprintln(os.Stderr, "Failed to serialise auth config:", err) - return "" - } - return base64.URLEncoding.EncodeToString(encoded) -} - -// Defaults to Supabase public ECR for faster image pull -const defaultRegistry = "public.ecr.aws" -const ghcrRegistry = "ghcr.io" - -func GetRegistry() string { - registry := viper.GetString("INTERNAL_IMAGE_REGISTRY") - if len(registry) == 0 { - return defaultRegistry - } - return strings.ToLower(registry) -} - -func HasRegistryOverride() bool { - return len(viper.GetString("INTERNAL_IMAGE_REGISTRY")) > 0 -} - -func GetRegistryImageUrl(imageName string) string { - registry := GetRegistry() - if registry == "docker.io" { - return imageName - } - // Configure mirror registry - parts := strings.Split(imageName, "/") - imageName = parts[len(parts)-1] - return registry + "/supabase/" + imageName -} - -func GetRegistryImageUrls(imageName string) []string { - if HasRegistryOverride() { - return []string{GetRegistryImageUrl(imageName)} - } - parts := strings.Split(imageName, "/") - lastPart := parts[len(parts)-1] - return dedupeStrings([]string{ - defaultRegistry + "/supabase/" + lastPart, - ghcrRegistry + "/supabase/" + lastPart, - dockerHubFallbackImage(imageName, lastPart), - }) -} - -func dockerHubFallbackImage(imageName, lastPart string) string { - if strings.HasPrefix(imageName, defaultRegistry+"/supabase/") || strings.HasPrefix(imageName, ghcrRegistry+"/supabase/") { - return "supabase/" + lastPart - } - return imageName -} - -func dedupeStrings(values []string) []string { - seen := make(map[string]struct{}, len(values)) - result := make([]string, 0, len(values)) - for _, value := range values { - if _, ok := seen[value]; ok { - continue - } - seen[value] = struct{}{} - result = append(result, value) - } - return result -} - -func registryFromImage(imageTag string) string { - parts := strings.Split(imageTag, "/") - if len(parts) == 1 { - return "docker.io" - } - first := parts[0] - if strings.Contains(first, ".") || strings.Contains(first, ":") || first == "localhost" { - return first - } - return "docker.io" -} - -func DockerImagePull(ctx context.Context, imageTag string, w io.Writer) error { - out, err := Docker.ImagePull(ctx, imageTag, image.PullOptions{ - RegistryAuth: GetRegistryAuthForImage(imageTag), - }) - if err != nil { - return errors.Errorf("failed to pull docker image: %w", err) - } - defer out.Close() - if err := jsonmessage.DisplayJSONMessagesToStream(out, streams.NewOut(w), nil); err != nil { - return errors.Errorf("failed to display json stream: %w", err) - } - return nil -} - -// Used by unit tests -var timeUnit = time.Second - -func DockerImagePullWithRetry(ctx context.Context, image string, retries int) error { - err := DockerImagePull(ctx, image, os.Stderr) - for i := range retries { - if err == nil || errors.Is(ctx.Err(), context.Canceled) { - break - } - fmt.Fprintln(os.Stderr, err) - period := time.Duration(2<<(i+1)) * timeUnit - fmt.Fprintf(os.Stderr, "Retrying after %v: %s\n", period, image) - time.Sleep(period) - err = DockerImagePull(ctx, image, os.Stderr) - } - return err -} - -func DockerPullImageIfNotCached(ctx context.Context, imageName string) error { - _, err := DockerResolveImageIfNotCached(ctx, imageName) - return err -} - -func DockerResolveImageIfNotCached(ctx context.Context, imageName string) (string, error) { - imageUrls := GetRegistryImageUrls(imageName) - for _, imageUrl := range imageUrls { - if _, err := Docker.ImageInspect(ctx, imageUrl); err == nil { - return imageUrl, nil - } else if !errdefs.IsNotFound(err) { - return "", errors.Errorf("failed to inspect docker image: %w", err) - } - } - var pullErrors []error - for _, imageUrl := range imageUrls { - err := DockerImagePullWithRetry(ctx, imageUrl, 2) - if err == nil { - return imageUrl, nil - } - pullErrors = append(pullErrors, fmt.Errorf("%s: %w", imageUrl, err)) - } - // errors.Join keeps every candidate's cause chain, so DockerStart's - // client.IsErrConnectionFailed unwrap still fires (errors.As traverses the - // Unwrap() []error tree) without flattening causes to a string or - // duplicating a representative error in the message. - return "", errors.Errorf("failed to pull docker image from all registries: %w", errors.Join(pullErrors...)) -} - -var suggestDockerInstall = "Docker Desktop is a prerequisite for local development. Follow the official docs to install: https://docs.docker.com/desktop" - -// SuggestDockerInstallIfConnectionFailed sets the install hint when err is a -// Docker daemon connection failure. Callers that resolve images before -// DockerStart (e.g. the concurrent ensureImagesCached pre-pull) call this once -// on the aggregated error, so the hint is surfaced without racing on the -// CmdSuggestion global from multiple goroutines. -func SuggestDockerInstallIfConnectionFailed(err error) { - if client.IsErrConnectionFailed(err) { - CmdSuggestion = suggestDockerInstall - } -} - -func DockerStart(ctx context.Context, config container.Config, hostConfig container.HostConfig, networkingConfig network.NetworkingConfig, containerName string) (string, error) { - // Pull container image - imageUrl, err := DockerResolveImageIfNotCached(ctx, config.Image) - if err != nil { - SuggestDockerInstallIfConnectionFailed(err) - return "", err - } - // Setup default config - config.Image = imageUrl - if config.Labels == nil { - config.Labels = make(map[string]string, 2) - } - config.Labels[CliProjectLabel] = Config.ProjectId - config.Labels[composeProjectLabel] = Config.ProjectId - // Configure container network - hostConfig.ExtraHosts = append(hostConfig.ExtraHosts, extraHosts...) - if networkId := viper.GetString("network-id"); len(networkId) > 0 { - hostConfig.NetworkMode = container.NetworkMode(networkId) - } else if len(hostConfig.NetworkMode) == 0 { - hostConfig.NetworkMode = container.NetworkMode(NetId) - } - if err := DockerNetworkCreateIfNotExists(ctx, hostConfig.NetworkMode, config.Labels); err != nil { - return "", err - } - // Configure container volumes - var binds, sources []string - for _, bind := range hostConfig.Binds { - spec, err := loader.ParseVolume(bind) - if err != nil { - return "", errors.Errorf("failed to parse docker volume: %w", err) - } - if spec.Type != string(mount.TypeVolume) { - binds = append(binds, bind) - } else if len(spec.Source) > 0 { - sources = append(sources, spec.Source) - } - } - // Skip named volume for BitBucket pipeline - if os.Getenv("BITBUCKET_CLONE_DIR") != "" { - hostConfig.Binds = binds - // Bitbucket doesn't allow for --security-opt option to be set - // https://support.atlassian.com/bitbucket-cloud/docs/run-docker-commands-in-bitbucket-pipelines/#Full-list-of-restricted-commands - hostConfig.SecurityOpt = nil - } else { - // Create named volumes with labels - for _, name := range sources { - if _, err := Docker.VolumeCreate(ctx, volume.CreateOptions{ - Name: name, - Labels: config.Labels, - }); err != nil { - return "", errors.Errorf("failed to create volume: %w", err) - } - } - } - // Create container from image - resp, err := Docker.ContainerCreate(ctx, &config, &hostConfig, &networkingConfig, nil, containerName) - if err != nil { - return "", errors.Errorf("failed to create docker container: %w", err) - } - // Run container in background - err = Docker.ContainerStart(ctx, resp.ID, container.StartOptions{}) - if err != nil { - if hostPort := parsePortBindError(err); len(hostPort) > 0 { - CmdSuggestion = suggestDockerStop(ctx, hostPort) - prefix := "Or configure" - if len(CmdSuggestion) == 0 { - prefix = "Try configuring" - } - name := containerName - if endpoint, ok := networkingConfig.EndpointsConfig[NetId]; ok && len(endpoint.Aliases) > 0 { - name = endpoint.Aliases[0] - } - CmdSuggestion += fmt.Sprintf("\n%s a different %s port in %s", prefix, name, Bold(ConfigPath)) - } - err = errors.Errorf("failed to start docker container %q: %w", containerName, err) - } - return resp.ID, err -} - -func DockerRemove(containerId string) { - if err := Docker.ContainerRemove(context.Background(), containerId, container.RemoveOptions{ - RemoveVolumes: true, - Force: true, - }); err != nil { - fmt.Fprintln(os.Stderr, "Failed to remove container:", containerId, err) - } -} - -type DockerJob struct { - Image string - Env []string - Cmd []string -} - -func DockerRunJob(ctx context.Context, job DockerJob, stdout, stderr io.Writer) error { - return DockerRunOnceWithStream(ctx, job.Image, job.Env, job.Cmd, stdout, stderr) -} - -// Runs a container image exactly once, returning stdout and throwing error on non-zero exit code. -func DockerRunOnce(ctx context.Context, image string, env []string, cmd []string) (string, error) { - stderr := GetDebugLogger() - var out bytes.Buffer - err := DockerRunOnceWithStream(ctx, image, env, cmd, &out, stderr) - return out.String(), err -} - -func DockerRunOnceWithStream(ctx context.Context, image string, env, cmd []string, stdout, stderr io.Writer) error { - return DockerRunOnceWithConfig(ctx, container.Config{ - Image: image, - Env: env, - Cmd: cmd, - }, container.HostConfig{}, network.NetworkingConfig{}, "", stdout, stderr) -} - -func DockerRunOnceWithConfig(ctx context.Context, config container.Config, hostConfig container.HostConfig, networkingConfig network.NetworkingConfig, containerName string, stdout, stderr io.Writer) error { - // Cannot rely on docker's auto remove because - // 1. We must inspect exit code after container stops - // 2. Context cancellation may happen after start - container, err := DockerStart(ctx, config, hostConfig, networkingConfig, containerName) - if err != nil { - return err - } - defer DockerRemove(container) - return DockerStreamLogs(ctx, container, stdout, stderr) -} - -// DockerRunOnceWaitWithConfig is like DockerRunOnceWithConfig but waits for the -// container to exit and then reads its already-buffered logs WITHOUT following -// the stream. -// -// DockerRunOnceWithConfig detects completion by reading a follow=true log stream -// until EOF. That EOF never arrives under podman: its /containers//logs?follow -// endpoint does not close the response when the container stops, so -// stdcopy.StdCopy blocks on the chunked HTTP body forever and the caller hangs at -// 0% CPU (supabase/pg-toolbelt#312). Waiting on /wait for the exit code and then -// reading the log once is reliable on both Docker and podman. -// -// Use this only for short-lived containers with bounded output (e.g. the -// edge-runtime pg-delta scripts): the full log is read after exit rather than -// streamed, so it is not suitable for large streaming output such as db dump. -func DockerRunOnceWaitWithConfig(ctx context.Context, config container.Config, hostConfig container.HostConfig, networkingConfig network.NetworkingConfig, containerName string, stdout, stderr io.Writer) error { - containerId, err := DockerStart(ctx, config, hostConfig, networkingConfig, containerName) - if err != nil { - return err - } - defer DockerRemove(containerId) - exitCode, err := dockerWaitExit(ctx, containerId) - if err != nil { - return err - } - if err := DockerStreamLogsOnce(ctx, containerId, stdout, stderr); err != nil { - return err - } - switch exitCode { - case 0: - return nil - case 137: - err = ErrContainerKilled - default: - err = errors.Errorf("exit %d", exitCode) - } - return errors.Errorf("error running container: %w", err) -} - -// dockerWaitInterval is how often dockerWaitExit polls container state. Kept -// short so bounded one-shot scripts return promptly; it is a package var so -// tests can drop it to zero. -var dockerWaitInterval = 200 * time.Millisecond - -// dockerWaitExit polls container state until it stops and returns its exit code. -// -// It deliberately uses ContainerInspect rather than a followed log stream (or -// /wait) to detect completion: inspect is reliable under podman, whereas -// podman's /logs?follow endpoint does not close when the container stops, which -// is what hangs DockerStreamLogs (see DockerRunOnceWaitWithConfig). Reusing -// inspect also keeps the request surface identical to DockerStreamLogs, so the -// existing test mocks continue to apply. -func dockerWaitExit(ctx context.Context, containerId string) (int64, error) { - for { - resp, err := Docker.ContainerInspect(ctx, containerId) - if err != nil { - return 0, errors.Errorf("failed to inspect docker container: %w", err) - } - if resp.State != nil && !resp.State.Running { - return int64(resp.State.ExitCode), nil - } - select { - case <-ctx.Done(): - return 0, ctx.Err() - case <-time.After(dockerWaitInterval): - } - } -} - -var ErrContainerKilled = errors.New("exit 137") - -func DockerStreamLogs(ctx context.Context, containerId string, stdout, stderr io.Writer, opts ...func(*container.LogsOptions)) error { - logsOptions := container.LogsOptions{ - ShowStdout: true, - ShowStderr: true, - Follow: true, - } - for _, apply := range opts { - apply(&logsOptions) - } - // Stream logs - logs, err := Docker.ContainerLogs(ctx, containerId, logsOptions) - if err != nil { - return errors.Errorf("failed to read docker logs: %w", err) - } - defer logs.Close() - if _, err := stdcopy.StdCopy(stdout, stderr, logs); err != nil { - return errors.Errorf("failed to copy docker logs: %w", err) - } - // Check exit code - resp, err := Docker.ContainerInspect(ctx, containerId) - if err != nil { - return errors.Errorf("failed to inspect docker container: %w", err) - } - switch resp.State.ExitCode { - case 0: - return nil - case 137: - err = ErrContainerKilled - default: - err = errors.Errorf("exit %d", resp.State.ExitCode) - } - return errors.Errorf("error running container: %w", err) -} - -func DockerStreamLogsOnce(ctx context.Context, containerId string, stdout, stderr io.Writer) error { - logs, err := Docker.ContainerLogs(ctx, containerId, container.LogsOptions{ - ShowStdout: true, - ShowStderr: true, - }) - if err != nil { - return errors.Errorf("failed to read docker logs: %w", err) - } - defer logs.Close() - if _, err := stdcopy.StdCopy(stdout, stderr, logs); err != nil { - return errors.Errorf("failed to copy docker logs: %w", err) - } - return nil -} - -// Exec a command once inside a container, returning stdout and throwing error on non-zero exit code. -func DockerExecOnce(ctx context.Context, containerId string, env []string, cmd []string) (string, error) { - stderr := io.Discard - if viper.GetBool("DEBUG") { - stderr = os.Stderr - } - var out bytes.Buffer - err := DockerExecOnceWithStream(ctx, containerId, "", env, cmd, &out, stderr) - return out.String(), err -} - -func DockerExecOnceWithStream(ctx context.Context, containerId, workdir string, env, cmd []string, stdout, stderr io.Writer) error { - // Reset shadow database - exec, err := Docker.ContainerExecCreate(ctx, containerId, container.ExecOptions{ - Env: env, - Cmd: cmd, - WorkingDir: workdir, - AttachStderr: true, - AttachStdout: true, - }) - if err != nil { - return errors.Errorf("failed to exec docker create: %w", err) - } - // Read exec output - resp, err := Docker.ContainerExecAttach(ctx, exec.ID, container.ExecStartOptions{}) - if err != nil { - return errors.Errorf("failed to exec docker attach: %w", err) - } - defer resp.Close() - // Capture error details - if _, err := stdcopy.StdCopy(stdout, stderr, resp.Reader); err != nil { - return errors.Errorf("failed to copy docker logs: %w", err) - } - // Get the exit code - iresp, err := Docker.ContainerExecInspect(ctx, exec.ID) - if err != nil { - return errors.Errorf("failed to exec docker inspect: %w", err) - } - if iresp.ExitCode > 0 { - err = errors.New("error executing command") - } - return err -} - -func IsDockerRunning(ctx context.Context) bool { - _, err := Docker.Ping(ctx) - return !client.IsErrConnectionFailed(err) -} - -var portErrorPattern = regexp.MustCompile("Bind for (.*) failed: port is already allocated") - -func parsePortBindError(err error) string { - matches := portErrorPattern.FindStringSubmatch(err.Error()) - if len(matches) > 1 { - return matches[len(matches)-1] - } - return "" -} - -func suggestDockerStop(ctx context.Context, hostPort string) string { - if containers, err := Docker.ContainerList(ctx, container.ListOptions{}); err == nil { - for _, c := range containers { - for _, p := range c.Ports { - if fmt.Sprintf("%s:%d", p.IP, p.PublicPort) == hostPort { - if project, ok := c.Labels[CliProjectLabel]; ok { - return "\nTry stopping the running project with " + Aqua("supabase stop --project-id "+project) - } else { - name := c.ID - if len(c.Names) > 0 { - name = c.Names[0] - } - return "\nTry stopping the running container with " + Aqua("docker stop "+name) - } - } - } - } - } - return "" -} diff --git a/apps/cli-go/internal/utils/docker_darwin.go b/apps/cli-go/internal/utils/docker_darwin.go deleted file mode 100644 index af183bc31a..0000000000 --- a/apps/cli-go/internal/utils/docker_darwin.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build darwin - -package utils - -import "github.com/docker/docker/api/types/container" - -var extraHosts []string - -func isUserDefined(mode container.NetworkMode) bool { - return mode.IsUserDefined() -} diff --git a/apps/cli-go/internal/utils/docker_linux.go b/apps/cli-go/internal/utils/docker_linux.go deleted file mode 100644 index b5463d0604..0000000000 --- a/apps/cli-go/internal/utils/docker_linux.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build linux - -package utils - -import "github.com/docker/docker/api/types/container" - -// Allows containers to resolve host network: https://stackoverflow.com/a/62431165 -var extraHosts = []string{DinDHost + ":host-gateway"} - -func isUserDefined(mode container.NetworkMode) bool { - return mode.IsUserDefined() -} diff --git a/apps/cli-go/internal/utils/docker_test.go b/apps/cli-go/internal/utils/docker_test.go deleted file mode 100644 index 5c58d5aea0..0000000000 --- a/apps/cli-go/internal/utils/docker_test.go +++ /dev/null @@ -1,386 +0,0 @@ -package utils - -import ( - "bytes" - "context" - "net/http" - "testing" - "time" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/pkg/stdcopy" - "github.com/h2non/gock" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/testing/apitest" -) - -const ( - containerId = "test-container" - imageId = "test-image" -) - -func TestPullImage(t *testing.T) { - viper.Set("INTERNAL_IMAGE_REGISTRY", "docker.io") - - t.Run("pulls image if missing", func(t *testing.T) { - // Setup mock docker - require.NoError(t, apitest.MockDocker(Docker)) - defer gock.OffAll() - gock.New(Docker.DaemonHost()). - Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json"). - Reply(http.StatusNotFound) - gock.New(Docker.DaemonHost()). - Post("/v"+Docker.ClientVersion()+"/images/create"). - MatchParam("fromImage", imageId). - MatchParam("tag", "latest"). - Reply(http.StatusAccepted) - // Run test - assert.NoError(t, DockerPullImageIfNotCached(context.Background(), imageId)) - // Validate api - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("does nothing if image exists", func(t *testing.T) { - // Setup mock docker - require.NoError(t, apitest.MockDocker(Docker)) - defer gock.OffAll() - gock.New(Docker.DaemonHost()). - Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json"). - Reply(http.StatusOK). - JSON(image.InspectResponse{}) - // Run test - assert.NoError(t, DockerPullImageIfNotCached(context.Background(), imageId)) - // Validate api - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error if docker is unavailable", func(t *testing.T) { - // Setup mock docker - require.NoError(t, apitest.MockDocker(Docker)) - defer gock.OffAll() - gock.New(Docker.DaemonHost()). - Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json"). - Reply(http.StatusServiceUnavailable) - // Run test - assert.Error(t, DockerPullImageIfNotCached(context.Background(), imageId)) - // Validate api - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on failure to pull image", func(t *testing.T) { - timeUnit = time.Duration(0) - // Setup mock docker - require.NoError(t, apitest.MockDocker(Docker)) - defer gock.OffAll() - gock.New(Docker.DaemonHost()). - Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json"). - Reply(http.StatusNotFound) - // Total 3 tries - gock.New(Docker.DaemonHost()). - Post("/v"+Docker.ClientVersion()+"/images/create"). - MatchParam("fromImage", imageId). - MatchParam("tag", "latest"). - Reply(http.StatusServiceUnavailable) - gock.New(Docker.DaemonHost()). - Post("/v"+Docker.ClientVersion()+"/images/create"). - MatchParam("fromImage", imageId). - MatchParam("tag", "latest"). - Reply(http.StatusAccepted). - JSON(jsonmessage.JSONMessage{Error: &jsonmessage.JSONError{Message: "toomanyrequests"}}) - gock.New(Docker.DaemonHost()). - Post("/v"+Docker.ClientVersion()+"/images/create"). - MatchParam("fromImage", imageId). - MatchParam("tag", "latest"). - Reply(http.StatusAccepted). - JSON(jsonmessage.JSONMessage{Error: &jsonmessage.JSONError{Message: "no space left on device"}}) - // Run test - err := DockerPullImageIfNotCached(context.Background(), imageId) - // Validate api - assert.ErrorContains(t, err, "no space left on device") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("falls back to GHCR when the default ECR pull fails", func(t *testing.T) { - viper.Set("INTERNAL_IMAGE_REGISTRY", "") - timeUnit = time.Duration(0) - t.Cleanup(func() { - viper.Set("INTERNAL_IMAGE_REGISTRY", "docker.io") - timeUnit = time.Second - }) - // Setup mock docker - require.NoError(t, apitest.MockDocker(Docker)) - defer gock.OffAll() - ecrImage := "public.ecr.aws/supabase/" + imageId - ghcrImage := "ghcr.io/supabase/" + imageId - dockerImage := imageId - for _, image := range []string{ecrImage, ghcrImage, dockerImage} { - gock.New(Docker.DaemonHost()). - Get("/v" + Docker.ClientVersion() + "/images/" + image + "/json"). - Reply(http.StatusNotFound) - } - // ECR gets the initial attempt plus two retries. - for range 3 { - gock.New(Docker.DaemonHost()). - Post("/v"+Docker.ClientVersion()+"/images/create"). - MatchParam("fromImage", ecrImage). - MatchParam("tag", "latest"). - Reply(http.StatusAccepted). - JSON(jsonmessage.JSONMessage{Error: &jsonmessage.JSONError{Message: "toomanyrequests: Rate exceeded"}}) - } - gock.New(Docker.DaemonHost()). - Post("/v"+Docker.ClientVersion()+"/images/create"). - MatchParam("fromImage", ghcrImage). - MatchParam("tag", "latest"). - Reply(http.StatusAccepted) - // Run test - imageURL, err := DockerResolveImageIfNotCached(context.Background(), imageId) - // Validate api - assert.NoError(t, err) - assert.Equal(t, ghcrImage, imageURL) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} - -func TestRegistryImageUrls(t *testing.T) { - t.Cleanup(func() { - viper.Set("INTERNAL_IMAGE_REGISTRY", "") - }) - - t.Run("returns fallback candidates when the registry is unset", func(t *testing.T) { - viper.Set("INTERNAL_IMAGE_REGISTRY", "") - - assert.Equal(t, []string{ - "public.ecr.aws/supabase/postgres:17.6.1.138", - "ghcr.io/supabase/postgres:17.6.1.138", - "supabase/postgres:17.6.1.138", - }, GetRegistryImageUrls("supabase/postgres:17.6.1.138")) - }) - - t.Run("dedupes an already-defaulted image", func(t *testing.T) { - viper.Set("INTERNAL_IMAGE_REGISTRY", "") - - assert.Equal(t, []string{ - "public.ecr.aws/supabase/postgres:17.6.1.138", - "ghcr.io/supabase/postgres:17.6.1.138", - "supabase/postgres:17.6.1.138", - }, GetRegistryImageUrls("public.ecr.aws/supabase/postgres:17.6.1.138")) - }) - - t.Run("uses a single candidate when the registry is explicitly configured", func(t *testing.T) { - viper.Set("INTERNAL_IMAGE_REGISTRY", "public.ecr.aws") - assert.Equal(t, []string{ - "public.ecr.aws/supabase/postgres:17.6.1.138", - }, GetRegistryImageUrls("supabase/postgres:17.6.1.138")) - - viper.Set("INTERNAL_IMAGE_REGISTRY", "docker.io") - assert.Equal(t, []string{ - "supabase/postgres:17.6.1.138", - }, GetRegistryImageUrls("supabase/postgres:17.6.1.138")) - }) -} - -func TestRunOnce(t *testing.T) { - viper.Set("INTERNAL_IMAGE_REGISTRY", "docker.io") - - t.Run("runs once in container", func(t *testing.T) { - // Setup mock docker - require.NoError(t, apitest.MockDocker(Docker)) - defer gock.OffAll() - apitest.MockDockerStart(Docker, imageId, containerId) - require.NoError(t, apitest.MockDockerLogs(Docker, containerId, "hello world")) - // Run test - out, err := DockerRunOnce(context.Background(), imageId, nil, nil) - assert.NoError(t, err) - // Validate api - assert.Equal(t, "hello world", out) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on container create", func(t *testing.T) { - // Setup mock docker - require.NoError(t, apitest.MockDocker(Docker)) - defer gock.OffAll() - gock.New(Docker.DaemonHost()). - Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json"). - Reply(http.StatusOK). - JSON(image.InspectResponse{}) - gock.New(Docker.DaemonHost()). - Post("/v" + Docker.ClientVersion() + "/networks/create"). - Reply(http.StatusCreated). - JSON(network.CreateResponse{}) - gock.New(Docker.DaemonHost()). - Post("/v" + Docker.ClientVersion() + "/containers/create"). - Reply(http.StatusServiceUnavailable) - // Run test - _, err := DockerRunOnce(context.Background(), imageId, nil, nil) - assert.Error(t, err) - // Validate api - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on container start", func(t *testing.T) { - // Setup mock docker - require.NoError(t, apitest.MockDocker(Docker)) - defer gock.OffAll() - gock.New(Docker.DaemonHost()). - Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json"). - Reply(http.StatusOK). - JSON(image.InspectResponse{}) - gock.New(Docker.DaemonHost()). - Post("/v" + Docker.ClientVersion() + "/networks/create"). - Reply(http.StatusCreated). - JSON(network.CreateResponse{}) - gock.New(Docker.DaemonHost()). - Post("/v" + Docker.ClientVersion() + "/containers/create"). - Reply(http.StatusOK). - JSON(container.CreateResponse{ID: containerId}) - gock.New(Docker.DaemonHost()). - Post("/v" + Docker.ClientVersion() + "/containers/" + containerId + "/start"). - Reply(http.StatusServiceUnavailable) - // Run test - _, err := DockerRunOnce(context.Background(), imageId, nil, nil) - assert.Error(t, err) - // Validate api - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("removes container on cancel", func(t *testing.T) { - // Setup mock docker - require.NoError(t, apitest.MockDocker(Docker)) - defer gock.OffAll() - apitest.MockDockerStart(Docker, imageId, containerId) - gock.New(Docker.DaemonHost()). - Get("/v"+Docker.ClientVersion()+"/containers/"+containerId+"/logs"). - Reply(http.StatusOK). - SetHeader("Content-Type", "application/vnd.docker.raw-stream"). - Delay(1 * time.Second) - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(200*time.Millisecond)) - defer cancel() - gock.New(Docker.DaemonHost()). - Delete("/v" + Docker.ClientVersion() + "/containers/" + containerId). - Reply(http.StatusOK) - // Run test - _, err := DockerRunOnce(ctx, imageId, nil, nil) - assert.Error(t, err) - // Validate api - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on failure to parse logs", func(t *testing.T) { - // Setup mock docker - require.NoError(t, apitest.MockDocker(Docker)) - defer gock.OffAll() - apitest.MockDockerStart(Docker, imageId, containerId) - gock.New(Docker.DaemonHost()). - Get("/v"+Docker.ClientVersion()+"/containers/"+containerId+"/logs"). - Reply(http.StatusOK). - SetHeader("Content-Type", "application/vnd.docker.raw-stream"). - BodyString("hello world") - gock.New(Docker.DaemonHost()). - Delete("/v" + Docker.ClientVersion() + "/containers/" + containerId). - Reply(http.StatusOK) - // Run test - _, err := DockerRunOnce(context.Background(), imageId, nil, nil) - assert.Error(t, err) - // Validate api - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on failure to inspect", func(t *testing.T) { - // Setup mock docker - require.NoError(t, apitest.MockDocker(Docker)) - defer gock.OffAll() - apitest.MockDockerStart(Docker, imageId, containerId) - // Setup docker style logs - var body bytes.Buffer - writer := stdcopy.NewStdWriter(&body, stdcopy.Stdout) - _, err := writer.Write([]byte("hello world")) - require.NoError(t, err) - gock.New("http:///var/run/docker.sock"). - Get("/v"+Docker.ClientVersion()+"/containers/"+containerId+"/logs"). - Reply(http.StatusOK). - SetHeader("Content-Type", "application/vnd.docker.raw-stream"). - Body(&body) - gock.New("http:///var/run/docker.sock"). - Get("/v" + Docker.ClientVersion() + "/containers/" + containerId + "/json"). - Reply(http.StatusServiceUnavailable) - gock.New(Docker.DaemonHost()). - Delete("/v" + Docker.ClientVersion() + "/containers/" + containerId). - Reply(http.StatusOK) - // Run test - _, err = DockerRunOnce(context.Background(), imageId, nil, nil) - assert.ErrorContains(t, err, "request returned 503 Service Unavailable for API route and version") - // Validate api - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on non-zero exit code", func(t *testing.T) { - // Setup mock docker - require.NoError(t, apitest.MockDocker(Docker)) - defer gock.OffAll() - apitest.MockDockerStart(Docker, imageId, containerId) - // Setup docker style logs - var body bytes.Buffer - writer := stdcopy.NewStdWriter(&body, stdcopy.Stdout) - _, err := writer.Write([]byte("hello world")) - require.NoError(t, err) - gock.New("http:///var/run/docker.sock"). - Get("/v"+Docker.ClientVersion()+"/containers/"+containerId+"/logs"). - Reply(http.StatusOK). - SetHeader("Content-Type", "application/vnd.docker.raw-stream"). - Body(&body) - gock.New("http:///var/run/docker.sock"). - Get("/v" + Docker.ClientVersion() + "/containers/" + containerId + "/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{ - ExitCode: 1, - }}}) - gock.New(Docker.DaemonHost()). - Delete("/v" + Docker.ClientVersion() + "/containers/" + containerId). - Reply(http.StatusOK) - // Run test - _, err = DockerRunOnce(context.Background(), imageId, nil, nil) - assert.ErrorContains(t, err, "error running container: exit 1") - // Validate api - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} - -func TestExecOnce(t *testing.T) { - t.Run("throws error on failure to exec", func(t *testing.T) { - // Setup mock server - require.NoError(t, apitest.MockDocker(Docker)) - defer gock.OffAll() - gock.New(Docker.DaemonHost()). - Post("/v" + Docker.ClientVersion() + "/containers/" + containerId + "/exec"). - Reply(http.StatusServiceUnavailable) - // Run test - _, err := DockerExecOnce(context.Background(), containerId, nil, nil) - assert.Error(t, err) - // Validate api - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on failure to hijack", func(t *testing.T) { - // Setup mock server - require.NoError(t, apitest.MockDocker(Docker)) - defer gock.OffAll() - gock.New(Docker.DaemonHost()). - Post("/v" + Docker.ClientVersion() + "/containers/" + containerId + "/exec"). - Reply(http.StatusAccepted). - JSON(container.ExecCreateResponse{ID: "test-command"}) - // Run test - _, err := DockerExecOnce(context.Background(), containerId, nil, nil) - assert.Error(t, err) - // Validate api - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - // TODO: mock tcp hijack -} diff --git a/apps/cli-go/internal/utils/docker_wait_test.go b/apps/cli-go/internal/utils/docker_wait_test.go deleted file mode 100644 index 22ee32e0ee..0000000000 --- a/apps/cli-go/internal/utils/docker_wait_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package utils - -import ( - "bytes" - "context" - "testing" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/network" - "github.com/h2non/gock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/testing/apitest" -) - -// DockerRunOnceWaitWithConfig must detect container completion via inspect and -// read logs without following the stream. Following the log stream to detect -// exit hangs forever under podman, whose /logs?follow endpoint never closes when -// the container stops (supabase/pg-toolbelt#312). These tests pin that the runner -// reads the captured output and maps the inspected exit code to an error. -func TestDockerRunOnceWait(t *testing.T) { - imageUrl := GetRegistryImageUrl(imageId) - - t.Run("waits for exit then reads buffered logs", func(t *testing.T) { - require.NoError(t, apitest.MockDocker(Docker)) - defer gock.OffAll() - apitest.MockDockerStart(Docker, imageUrl, containerId) - require.NoError(t, apitest.MockDockerLogs(Docker, containerId, "CATALOG\n")) - // Run test - var stdout, stderr bytes.Buffer - err := DockerRunOnceWaitWithConfig( - context.Background(), - container.Config{Image: imageUrl}, - container.HostConfig{}, - network.NetworkingConfig{}, - containerId, - &stdout, - &stderr, - ) - // Validate - assert.NoError(t, err) - assert.Equal(t, "CATALOG\n", stdout.String()) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("maps non-zero exit code to error", func(t *testing.T) { - require.NoError(t, apitest.MockDocker(Docker)) - defer gock.OffAll() - apitest.MockDockerStart(Docker, imageUrl, containerId) - require.NoError(t, apitest.MockDockerLogsExitCode(Docker, containerId, 1)) - // Run test - var stdout, stderr bytes.Buffer - err := DockerRunOnceWaitWithConfig( - context.Background(), - container.Config{Image: imageUrl}, - container.HostConfig{}, - network.NetworkingConfig{}, - containerId, - &stdout, - &stderr, - ) - // Validate - assert.ErrorContains(t, err, "error running container: exit 1") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} diff --git a/apps/cli-go/internal/utils/docker_windows.go b/apps/cli-go/internal/utils/docker_windows.go deleted file mode 100644 index 99e68d9b69..0000000000 --- a/apps/cli-go/internal/utils/docker_windows.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build windows - -package utils - -import ( - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/network" -) - -var extraHosts []string - -func isUserDefined(mode container.NetworkMode) bool { - // Host network requires explicit check on windows: https://github.com/supabase/cli/pull/952 - return mode.IsUserDefined() && mode.UserDefined() != network.NetworkHost -} diff --git a/apps/cli-go/internal/utils/edgeruntime.go b/apps/cli-go/internal/utils/edgeruntime.go deleted file mode 100644 index 8e54afa628..0000000000 --- a/apps/cli-go/internal/utils/edgeruntime.go +++ /dev/null @@ -1,164 +0,0 @@ -package utils - -import ( - "bytes" - "context" - "fmt" - "net" - "strings" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/network" - "github.com/go-errors/errors" - "github.com/spf13/viper" -) - -// EdgeRuntimeScriptErrorSentinel is printed to stderr by the pg-delta Deno -// templates when their body throws (see the `catch` blocks in -// internal/db/diff/templates/*.ts). The templates force the edge-runtime worker -// to exit by throwing on both the success and failure paths, and that non-zero -// exit is otherwise suppressed here when stderr contains "main worker has been -// destroyed". Without a distinct marker a crashed script would be -// indistinguishable from a successful empty diff, so `db pull` would report "No -// schema changes found" while the real error (e.g. a permission-denied catalog -// query) was silently swallowed. Templates and tests must reference this exact -// string. See supabase/cli#5826. -const EdgeRuntimeScriptErrorSentinel = "PGDELTA_SCRIPT_ERROR" - -// edgeRuntimeFile is a single file dropped into the edge-runtime container's -// working directory before the configured command is run. -type edgeRuntimeFile struct { - name string - content string -} - -// edgeRuntimeOptions accumulates the optional inputs assembled by -// EdgeRuntimeOption functions and consumed by RunEdgeRuntimeScript. -type edgeRuntimeOptions struct { - extraFiles []edgeRuntimeFile - extraEnv []string -} - -// EdgeRuntimeOption customizes a RunEdgeRuntimeScript invocation. The current -// shape (extra files dropped alongside index.ts, extra container env vars) -// covers the local-pg-delta use case; extend the option struct as new needs -// arrive instead of adding more positional arguments. -type EdgeRuntimeOption func(*edgeRuntimeOptions) - -// WithExtraFile schedules an extra file alongside `index.ts` in the container. -// Useful for project-local config files (e.g. `.npmrc`, `deno.json`) that need -// to live next to the script Deno is asked to run. -func WithExtraFile(name, content string) EdgeRuntimeOption { - return func(o *edgeRuntimeOptions) { - o.extraFiles = append(o.extraFiles, edgeRuntimeFile{name: name, content: content}) - } -} - -// WithExtraEnv appends container env entries in `KEY=value` form. -func WithExtraEnv(entries ...string) EdgeRuntimeOption { - return func(o *edgeRuntimeOptions) { - o.extraEnv = append(o.extraEnv, entries...) - } -} - -// getFreeHostPort asks the OS for an unused TCP port on the host. -func getFreeHostPort() (int, error) { - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - return 0, errors.Errorf("failed to allocate free port: %w", err) - } - defer listener.Close() - return listener.Addr().(*net.TCPAddr).Port, nil -} - -// EdgeRuntimeStartCmd builds the base command for launching a one-shot Edge -// Runtime script. The runtime's HTTP listener is bound to a free host port so -// concurrent or leftover containers (which share the host network namespace -// because diff containers run with NetworkMode=host) don't collide on the -// edge-runtime default port, which surfaces as "Address already in use (os -// error 98)". See https://github.com/supabase/cli/issues/5407. -func EdgeRuntimeStartCmd() []string { - cmd := []string{"edge-runtime", "start", "--main-service=."} - // Skip the flag on the rare allocation failure to preserve prior behavior. - if port, err := getFreeHostPort(); err == nil { - cmd = append(cmd, fmt.Sprintf("--port=%d", port)) - } - return cmd -} - -// RunEdgeRuntimeScript executes a TypeScript program inside the configured Edge -// Runtime container and streams stdout/stderr back to the caller. -func RunEdgeRuntimeScript(ctx context.Context, env []string, script string, binds []string, errPrefix string, stdout, stderr *bytes.Buffer, opts ...EdgeRuntimeOption) error { - state := &edgeRuntimeOptions{} - for _, opt := range opts { - if opt != nil { - opt(state) - } - } - cmd := EdgeRuntimeStartCmd() - if viper.GetBool("DEBUG") { - cmd = append(cmd, "--verbose") - } - cmdString := strings.Join(cmd, " ") - files := append([]edgeRuntimeFile{{name: "index.ts", content: script}}, state.extraFiles...) - entrypoint := []string{"sh", "-c", buildEdgeRuntimeEntrypoint(files, cmdString)} - combinedEnv := env - if len(state.extraEnv) > 0 { - combinedEnv = append(append([]string{}, env...), state.extraEnv...) - } - // Wait for the container to exit and then read its logs, rather than - // following the log stream to detect completion. The edge-runtime worker is - // forced to exit once the script flushes its output, but podman's - // /logs?follow endpoint does not close when the container stops, so a - // followed read (DockerRunOnceWithConfig) hangs the CLI forever - // (supabase/pg-toolbelt#312). pg-delta script output is bounded, so reading - // the log once after exit is safe. - if err := DockerRunOnceWaitWithConfig( - ctx, - container.Config{ - Image: Config.EdgeRuntime.Image, - Env: combinedEnv, - Entrypoint: entrypoint, - }, - container.HostConfig{ - Binds: binds, - NetworkMode: network.NetworkHost, - }, - network.NetworkingConfig{}, - "", - stdout, - stderr, - ); err != nil && !strings.Contains(stderr.String(), "main worker has been destroyed") { - return errors.Errorf("%s: %w:\n%s", errPrefix, err, stderr.String()) - } - // The templates suppress their own non-zero exit (they throw to force the - // worker to exit, which surfaces as "main worker has been destroyed"), so a - // script crash can slip past the check above. Treat the sentinel — printed - // only by the templates' catch blocks — as a hard failure so the real error, - // collected in stderr, reaches the user instead of looking like an empty diff. - if strings.Contains(stderr.String(), EdgeRuntimeScriptErrorSentinel) { - return errors.Errorf("%s: error running script:\n%s", errPrefix, stderr.String()) - } - return nil -} - -// buildEdgeRuntimeEntrypoint emits a `sh -c` body that writes each file via a -// here-document and then runs cmd. All heredoc openers are joined with `&&` -// before the bodies so bash stacks them in declaration order; each body is -// terminated with a unique sentinel so file contents can contain `EOF` safely. -func buildEdgeRuntimeEntrypoint(files []edgeRuntimeFile, cmd string) string { - if len(files) == 0 { - return cmd + "\n" - } - var head strings.Builder - var bodies strings.Builder - for i, f := range files { - sentinel := fmt.Sprintf("__EDGE_RT_FILE_%d__", i) - fmt.Fprintf(&head, "cat <<'%s' > %s && ", sentinel, f.name) - fmt.Fprintf(&bodies, "%s\n%s\n", f.content, sentinel) - } - head.WriteString(cmd) - head.WriteString("\n") - head.WriteString(bodies.String()) - return head.String() -} diff --git a/apps/cli-go/internal/utils/edgeruntime_test.go b/apps/cli-go/internal/utils/edgeruntime_test.go deleted file mode 100644 index 2497630255..0000000000 --- a/apps/cli-go/internal/utils/edgeruntime_test.go +++ /dev/null @@ -1,168 +0,0 @@ -package utils - -import ( - "bytes" - "context" - "io" - "net/http" - "strconv" - "strings" - "testing" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/pkg/stdcopy" - "github.com/h2non/gock" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/testing/apitest" -) - -// mockEdgeRuntimeLogs registers the docker responses RunEdgeRuntimeScript needs: -// a one-shot log read multiplexing stdout+stderr, an inspect reporting the exit -// code, and the container delete. Mirrors apitest.MockDockerErrorLogs but also -// carries stdout so we can assert the success path preserves the script output. -func mockEdgeRuntimeLogs(t *testing.T, containerID, stdout, stderr string, exitCode int) { - t.Helper() - var body bytes.Buffer - if len(stdout) > 0 { - _, err := io.Copy(stdcopy.NewStdWriter(&body, stdcopy.Stdout), strings.NewReader(stdout)) - require.NoError(t, err) - } - if len(stderr) > 0 { - _, err := io.Copy(stdcopy.NewStdWriter(&body, stdcopy.Stderr), strings.NewReader(stderr)) - require.NoError(t, err) - } - gock.New(Docker.DaemonHost()). - Get("/v"+Docker.ClientVersion()+"/containers/"+containerID+"/logs"). - Reply(http.StatusOK). - SetHeader("Content-Type", "application/vnd.docker.raw-stream"). - Body(&body) - gock.New(Docker.DaemonHost()). - Get("/v" + Docker.ClientVersion() + "/containers/" + containerID + "/json"). - Reply(http.StatusOK). - JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{ExitCode: exitCode}, - }}) - gock.New(Docker.DaemonHost()). - Delete("/v" + Docker.ClientVersion() + "/containers/" + containerID). - Reply(http.StatusOK) -} - -func TestRunEdgeRuntimeScript(t *testing.T) { - const containerID = "test-edge-runtime" - imageUrl := GetRegistryImageUrl(Config.EdgeRuntime.Image) - - t.Run("surfaces the real error when the script crashes behind the worker-destroyed message", func(t *testing.T) { - viper.Set("INTERNAL_IMAGE_REGISTRY", "docker.io") - t.Cleanup(func() { viper.Set("INTERNAL_IMAGE_REGISTRY", "") }) - require.NoError(t, apitest.MockDocker(Docker)) - defer gock.OffAll() - apitest.MockDockerStart(Docker, imageUrl, containerID) - // The pg-delta template throws to force the worker to exit (surfacing as a - // non-zero exit + "main worker has been destroyed"), and its catch block - // prints the real error and the sentinel. This must NOT look like an empty diff. - stderr := "error: permission denied for table pg_user_mapping\n" + - EdgeRuntimeScriptErrorSentinel + "\n" + - "worker boot error\nmain worker has been destroyed\n" - mockEdgeRuntimeLogs(t, containerID, "", stderr, 1) - - var stdout, stderrBuf bytes.Buffer - err := RunEdgeRuntimeScript(context.Background(), nil, "console.log('x')", nil, "error diffing schema", &stdout, &stderrBuf) - require.Error(t, err) - assert.Contains(t, err.Error(), "error diffing schema: error running script:") - // The real, actionable error must reach the user, not "No schema changes found". - assert.Contains(t, err.Error(), "permission denied for table pg_user_mapping") - }) - - t.Run("still ignores a worker-destroyed exit when no sentinel is present", func(t *testing.T) { - viper.Set("INTERNAL_IMAGE_REGISTRY", "docker.io") - t.Cleanup(func() { viper.Set("INTERNAL_IMAGE_REGISTRY", "") }) - require.NoError(t, apitest.MockDocker(Docker)) - defer gock.OffAll() - apitest.MockDockerStart(Docker, imageUrl, containerID) - // Success path: the template forces the worker to exit after writing output, - // so the exit is non-zero with "main worker has been destroyed" but no sentinel. - mockEdgeRuntimeLogs(t, containerID, "ALTER TABLE x;\n", "main worker has been destroyed\n", 1) - - var stdout, stderrBuf bytes.Buffer - err := RunEdgeRuntimeScript(context.Background(), nil, "console.log('x')", nil, "error diffing schema", &stdout, &stderrBuf) - require.NoError(t, err) - assert.Equal(t, "ALTER TABLE x;\n", stdout.String()) - }) -} - -func TestBuildEdgeRuntimeEntrypoint(t *testing.T) { - t.Run("emits a single heredoc when only the script is provided", func(t *testing.T) { - got := buildEdgeRuntimeEntrypoint( - []edgeRuntimeFile{{name: "index.ts", content: "console.log('hi')"}}, - "edge-runtime start --main-service=.", - ) - assert.True(t, strings.HasPrefix(got, "cat <<'__EDGE_RT_FILE_0__' > index.ts && edge-runtime start --main-service=.\n")) - assert.Contains(t, got, "console.log('hi')\n__EDGE_RT_FILE_0__\n") - }) - - t.Run("chains heredocs in declaration order so each cat reads the matching body", func(t *testing.T) { - got := buildEdgeRuntimeEntrypoint( - []edgeRuntimeFile{ - {name: "index.ts", content: "TS_CONTENT"}, - {name: ".npmrc", content: "NPMRC_CONTENT"}, - }, - "edge-runtime start --main-service=.", - ) - // Both cat declarations must come before any body, separated by &&. - assert.Contains(t, got, "cat <<'__EDGE_RT_FILE_0__' > index.ts && cat <<'__EDGE_RT_FILE_1__' > .npmrc && edge-runtime start --main-service=.") - // Bodies must follow in the same order as the declarations. - idxScript := strings.Index(got, "TS_CONTENT") - idxNpmrc := strings.Index(got, "NPMRC_CONTENT") - require.Greater(t, idxScript, 0) - require.Greater(t, idxNpmrc, idxScript, ".npmrc body must come after index.ts body") - // Sentinels close each body so user content containing `EOF` cannot - // terminate the heredoc early. - assert.Contains(t, got, "TS_CONTENT\n__EDGE_RT_FILE_0__") - assert.Contains(t, got, "NPMRC_CONTENT\n__EDGE_RT_FILE_1__") - assert.True(t, strings.HasSuffix(got, "\n")) - }) - - t.Run("returns just the command when no files are provided", func(t *testing.T) { - got := buildEdgeRuntimeEntrypoint(nil, "edge-runtime start --main-service=.") - assert.Equal(t, "edge-runtime start --main-service=.\n", got) - }) -} - -func TestEdgeRuntimeStartCmd(t *testing.T) { - t.Run("binds an explicit free port", func(t *testing.T) { - cmd := EdgeRuntimeStartCmd() - // Base command must always be present. - assert.Equal(t, []string{"edge-runtime", "start", "--main-service=."}, cmd[:3]) - // A --port flag avoids collisions on the edge-runtime default port (#5407). - var portFlag string - for _, arg := range cmd { - if strings.HasPrefix(arg, "--port=") { - portFlag = arg - } - } - require.NotEmpty(t, portFlag, "expected a --port flag to be set") - port, err := strconv.Atoi(strings.TrimPrefix(portFlag, "--port=")) - require.NoError(t, err) - assert.Greater(t, port, 0) - assert.LessOrEqual(t, port, 65535) - }) - - t.Run("allocates a distinct port per invocation", func(t *testing.T) { - first := getPortArg(t, EdgeRuntimeStartCmd()) - second := getPortArg(t, EdgeRuntimeStartCmd()) - assert.NotEqual(t, first, second) - }) -} - -func getPortArg(t *testing.T, cmd []string) string { - t.Helper() - for _, arg := range cmd { - if strings.HasPrefix(arg, "--port=") { - return arg - } - } - require.FailNow(t, "missing --port flag") - return "" -} diff --git a/apps/cli-go/internal/utils/enum.go b/apps/cli-go/internal/utils/enum.go deleted file mode 100644 index 06cac719d1..0000000000 --- a/apps/cli-go/internal/utils/enum.go +++ /dev/null @@ -1,35 +0,0 @@ -package utils - -import ( - "fmt" - "slices" - "strings" - - "github.com/go-errors/errors" -) - -// Ref: https://github.com/spf13/pflag/issues/236#issuecomment-931600452 -type EnumFlag struct { - Allowed []string - Value string -} - -func (a EnumFlag) String() string { - return a.Value -} - -func (a *EnumFlag) Set(p string) error { - if !slices.Contains(a.Allowed, p) { - return errors.Errorf("must be one of [ %s ]", strings.Join(a.Allowed, " | ")) - } - a.Value = p - return nil -} - -func (a *EnumFlag) Type() string { - values := strings.Join(a.Allowed, " | ") - if len(values) < 40 { - return fmt.Sprintf("[ %s ]", values) - } - return "string" -} diff --git a/apps/cli-go/internal/utils/flags/config_path.go b/apps/cli-go/internal/utils/flags/config_path.go deleted file mode 100644 index 4df1bff746..0000000000 --- a/apps/cli-go/internal/utils/flags/config_path.go +++ /dev/null @@ -1,26 +0,0 @@ -package flags - -import ( - "strings" - - "github.com/spf13/afero" - "github.com/supabase/cli/internal/utils" -) - -func LoadConfig(fsys afero.Fs) error { - utils.Config.ProjectId = ProjectRef - if err := utils.Config.Load("", utils.NewRootFS(fsys)); err != nil { - return err - } - utils.UpdateDockerIds() - // Apply profile specific overrides - if strings.EqualFold(utils.CurrentProfile.Name, "snap") { - ext := utils.Config.Auth.External["snapchat"] - ext.Enabled = true - ext.ClientId = utils.CurrentProfile.AuthClientID - // Any dummy value should work for local dev - ext.Secret.Value = utils.CurrentProfile.AuthClientID - utils.Config.Auth.External["snapchat"] = ext - } - return nil -} diff --git a/apps/cli-go/internal/utils/flags/db_url.go b/apps/cli-go/internal/utils/flags/db_url.go deleted file mode 100644 index bbfda766d6..0000000000 --- a/apps/cli-go/internal/utils/flags/db_url.go +++ /dev/null @@ -1,293 +0,0 @@ -package flags - -import ( - "context" - "crypto/rand" - _ "embed" - "fmt" - "math/big" - "net" - "net/http" - "os" - "strings" - "time" - - "github.com/cenkalti/backoff/v4" - "github.com/go-errors/errors" - "github.com/jackc/pgconn" - "github.com/spf13/afero" - "github.com/spf13/pflag" - "github.com/spf13/viper" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/internal/utils/credentials" - "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/config" -) - -type connection int - -const ( - unknown connection = iota - direct - local - linked - proxy -) - -var DbConfig pgconn.Config - -// PoolerFallbackEligible reports whether DbConfig was resolved via the linked -// path, where transparently retrying a failed remote operation through the -// project's IPv4 transaction pooler is safe. Explicit --db-url and --local -// targets must never be silently rerouted, so the fallback stays disabled for -// them. -var PoolerFallbackEligible bool - -func ParseDatabaseConfig(ctx context.Context, flagSet *pflag.FlagSet, fsys afero.Fs) error { - // Changed flags take precedence over default values - var connType connection - if flag := flagSet.Lookup("db-url"); flag != nil && flag.Changed { - connType = direct - } else if flag := flagSet.Lookup("local"); flag != nil && flag.Changed { - connType = local - } else if flag := flagSet.Lookup("linked"); flag != nil && flag.Changed { - connType = linked - } else if flag := flagSet.Lookup("proxy"); flag != nil && flag.Changed { - connType = proxy - } else if value, err := flagSet.GetBool("local"); err == nil && value { - connType = local - } else if value, err := flagSet.GetBool("linked"); err == nil && value { - connType = linked - } else if value, err := flagSet.GetBool("proxy"); err == nil && value { - connType = proxy - } - // Update connection config - switch connType { - case direct: - if err := LoadConfig(fsys); err != nil { - return err - } - if flag := flagSet.Lookup("db-url"); flag != nil { - config, err := pgconn.ParseConfig(flag.Value.String()) - if err != nil { - return errors.Errorf("failed to parse connection string: %w", err) - } - DbConfig = *config - } - case local: - if err := LoadConfig(fsys); err != nil { - return err - } - // Ignore other PG settings - DbConfig.Host = utils.Config.Hostname - DbConfig.Port = utils.Config.Db.Port - DbConfig.User = "postgres" - DbConfig.Password = utils.Config.Db.Password - DbConfig.Database = "postgres" - case linked: - if err := LoadProjectRef(fsys); err != nil { - return err - } - if err := LoadConfig(fsys); err != nil { - return err - } - var err error - if DbConfig, err = NewDbConfigWithPassword(ctx, ProjectRef); err != nil { - return err - } - case proxy: - token, err := utils.LoadAccessTokenFS(fsys) - if err != nil { - return err - } - if err := LoadProjectRef(fsys); err != nil { - return err - } - DbConfig.Host = utils.GetSupabaseAPIHost() - DbConfig.Port = 443 - DbConfig.User = "postgres" - DbConfig.Password = token - DbConfig.Database = ProjectRef - } - // Only the linked path may be transparently rerouted to the IPv4 pooler. - PoolerFallbackEligible = connType == linked - return nil -} - -const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - -func RandomString(size int) (string, error) { - data := make([]byte, size) - _, err := rand.Read(data) - if err != nil { - return "", errors.Errorf("failed to read random: %w", err) - } - for i := range data { - n := int(data[i]) % len(letters) - data[i] = letters[n] - } - return string(data), nil -} - -func NewDbConfigWithPassword(ctx context.Context, projectRef string) (pgconn.Config, error) { - config := pgconn.Config{ - Host: utils.GetSupabaseDbHost(projectRef), - Port: 5432, - User: "postgres", - Password: viper.GetString("DB_PASSWORD"), - Database: "postgres", - } - logger := utils.GetDebugLogger() - // Use pooler if host is not reachable directly - d := net.Dialer{Timeout: 5 * time.Second} - addr := fmt.Sprintf("%s:%d", config.Host, config.Port) - if conn, err := d.DialContext(ctx, "tcp", addr); err == nil { - if err := conn.Close(); err != nil { - fmt.Fprintln(logger, err) - } - fmt.Fprintf(logger, "Resolved DNS: %v\n", conn.RemoteAddr()) - } else if poolerConfig := utils.GetPoolerConfig(projectRef); poolerConfig != nil { - if len(config.Password) > 0 { - fmt.Fprintln(logger, "Using database password from env var...") - poolerConfig.Password = config.Password - } else if err := initPoolerLogin(ctx, projectRef, poolerConfig); err != nil { - utils.SetConnectSuggestion(err) - if utils.CmdSuggestion == "" { - utils.CmdSuggestion = utils.SuggestEnvVar - } - return *poolerConfig, err - } - return *poolerConfig, nil - } else { - utils.CmdSuggestion = fmt.Sprintf("Run %s to setup IPv4 connection.", utils.Aqua("supabase link --project-ref "+projectRef)) - return config, errors.Errorf("IPv6 is not supported on your current network: %w", err) - } - // Connect via direct connection - if len(config.Password) > 0 { - fmt.Fprintln(logger, "Using database password from env var...") - } else if err := initLoginRole(ctx, projectRef, &config); err != nil { - // Do not prompt because reading masked input is buggy on windows - utils.CmdSuggestion = utils.SuggestEnvVar - return config, err - } - return config, nil -} - -// ResolvePoolerConfigForFallback returns an authenticated IPv4 transaction -// pooler connection config for the given linked project. It prefers the pooler -// URL persisted at link time and falls back to fetching it from the Management -// API. This is used to transparently retry remote pg operations that failed -// because the direct database host was unreachable over IPv6. -func ResolvePoolerConfigForFallback(ctx context.Context, projectRef string) (pgconn.Config, error) { - poolerConfig := utils.GetPoolerConfig(projectRef) - if poolerConfig == nil { - primary, err := utils.GetPoolerConfigPrimary(ctx, projectRef) - if err != nil { - return pgconn.Config{}, err - } - poolerConfig, err = utils.ParsePoolerURL(primary.ConnectionString) - if err != nil { - return pgconn.Config{}, err - } - // Supavisor transaction mode does not support prepared statements. - poolerConfig.Port = 5432 - } - if password := viper.GetString("DB_PASSWORD"); len(password) > 0 { - poolerConfig.Password = password - } else if err := initPoolerLogin(ctx, projectRef, poolerConfig); err != nil { - return *poolerConfig, err - } - return *poolerConfig, nil -} - -func initLoginRole(ctx context.Context, projectRef string, config *pgconn.Config) error { - fmt.Fprintln(os.Stderr, "Initialising login role...") - body := api.CreateRoleBody{ReadOnly: false} - resp, err := utils.GetSupabase().V1CreateLoginRoleWithResponse(ctx, projectRef, body) - if err != nil { - return errors.Errorf("failed to initialise login role: %w", err) - } else if resp.JSON201 == nil { - return errors.Errorf("unexpected login role status %d: %s", resp.StatusCode(), string(resp.Body)) - } - config.User = resp.JSON201.Role - config.Password = resp.JSON201.Password - return nil -} - -func initPoolerLogin(ctx context.Context, projectRef string, poolerConfig *pgconn.Config) error { - poolerUser := poolerConfig.User - if err := initLoginRole(ctx, projectRef, poolerConfig); err != nil { - return err - } - suffix := "." + projectRef - if strings.HasSuffix(poolerUser, suffix) { - poolerConfig.User += suffix - } - // Wait for pooler to refresh password - login := func() error { - conn, err := pgconn.ConnectConfig(ctx, poolerConfig) - if err != nil { - return errors.Errorf("failed to connect as temp role: %w", err) - } - return conn.Close(ctx) - } - notify := utils.NewErrorCallback(func(attempt uint) error { - if attempt < 3 { - return nil - } - if ips, err := ListNetworkBans(ctx, projectRef); err != nil { - return err - } else if len(ips) > 0 { - return UnbanIP(ctx, projectRef, ips...) - } - return nil - }) - return backoff.RetryNotify(login, utils.NewBackoffPolicy(ctx), notify) -} - -func ListNetworkBans(ctx context.Context, projectRef string) ([]string, error) { - resp, err := utils.GetSupabase().V1ListAllNetworkBansWithResponse(ctx, projectRef) - if err != nil { - return nil, errors.Errorf("failed to list network bans: %w", err) - } else if resp.JSON201 == nil { - return nil, errors.Errorf("unexpected list bans status %d: %s", resp.StatusCode(), string(resp.Body)) - } - return resp.JSON201.BannedIpv4Addresses, nil -} - -func UnbanIP(ctx context.Context, projectRef string, addrs ...string) error { - includeSelf := len(addrs) == 0 - body := api.RemoveNetworkBanRequest{ - Ipv4Addresses: append([]string{}, addrs...), - RequesterIp: &includeSelf, - } - if resp, err := utils.GetSupabase().V1DeleteNetworkBansWithResponse(ctx, projectRef, body); err != nil { - return errors.Errorf("failed to remove network bans: %w", err) - } else if resp.StatusCode() != http.StatusOK { - return errors.Errorf("unexpected unban status %d: %s", resp.StatusCode(), string(resp.Body)) - } - return nil -} - -const PASSWORD_LENGTH = 16 - -func PromptPassword(stdin *os.File) string { - fmt.Fprint(os.Stderr, "Enter your database password (or leave blank to generate one): ") - if input := credentials.PromptMasked(stdin); len(input) > 0 { - return input - } - // Generate a password, see ./Settings/Database/DatabaseSettings/ResetDbPassword.tsx#L83 - var password []byte - charset := string(config.LowerUpperLettersDigits.ToChar()) - charset = strings.ReplaceAll(charset, ":", "") - maxRange := big.NewInt(int64(len(charset))) - for range PASSWORD_LENGTH { - random, err := rand.Int(rand.Reader, maxRange) - if err != nil { - fmt.Fprintln(os.Stderr, "Failed to randomise password:", err) - continue - } - password = append(password, charset[random.Int64()]) - } - return string(password) -} diff --git a/apps/cli-go/internal/utils/flags/db_url_test.go b/apps/cli-go/internal/utils/flags/db_url_test.go deleted file mode 100644 index 7343be6519..0000000000 --- a/apps/cli-go/internal/utils/flags/db_url_test.go +++ /dev/null @@ -1,180 +0,0 @@ -package flags - -import ( - "context" - "fmt" - "net/http" - "os" - "strings" - "testing" - - "github.com/h2non/gock" - "github.com/spf13/afero" - "github.com/spf13/pflag" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/testing/apitest" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/api" -) - -func TestParseDatabaseConfig(t *testing.T) { - // Setup valid access token - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) - - t.Run("parses direct connection from db-url flag", func(t *testing.T) { - flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) - flagSet.String("db-url", "postgres://postgres:password@localhost:5432/postgres", "") - err := flagSet.Set("db-url", "postgres://admin:secret@db.example.com:6432/app") - assert.Nil(t, err) - - fsys := afero.NewMemMapFs() - - err = ParseDatabaseConfig(context.Background(), flagSet, fsys) - - assert.NoError(t, err) - assert.Equal(t, "db.example.com", DbConfig.Host) - assert.Equal(t, uint16(6432), DbConfig.Port) - assert.Equal(t, "admin", DbConfig.User) - assert.Equal(t, "secret", DbConfig.Password) - assert.Equal(t, "app", DbConfig.Database) - }) - - t.Run("parses local connection", func(t *testing.T) { - flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) - flagSet.Bool("local", false, "") - err := flagSet.Set("local", "true") - assert.Nil(t, err) - - fsys := afero.NewMemMapFs() - - utils.Config.Hostname = "localhost" - utils.Config.Db.Port = 54322 - utils.Config.Db.Password = "local-password" - - err = ParseDatabaseConfig(context.Background(), flagSet, fsys) - - assert.NoError(t, err) - assert.Equal(t, "localhost", DbConfig.Host) - assert.Equal(t, uint16(54322), DbConfig.Port) - assert.Equal(t, "postgres", DbConfig.User) - assert.Equal(t, "local-password", DbConfig.Password) - assert.Equal(t, "postgres", DbConfig.Database) - }) - - t.Run("parses linked connection", func(t *testing.T) { - flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) - flagSet.Bool("linked", false, "") - err := flagSet.Set("linked", "true") - assert.Nil(t, err) - - fsys := afero.NewMemMapFs() - - project := apitest.RandomProjectRef() - err = afero.WriteFile(fsys, utils.ProjectRefPath, []byte(project), 0644) - require.NoError(t, err) - - dbURL := fmt.Sprintf("postgres://postgres:postgres@db.%s.supabase.co:6543/postgres", project) - err = afero.WriteFile(fsys, utils.PoolerUrlPath, []byte(dbURL), 0644) - require.NoError(t, err) - - viper.Set("DB_PASSWORD", "test") - err = ParseDatabaseConfig(context.Background(), flagSet, fsys) - - assert.NoError(t, err) - assert.True(t, strings.HasPrefix(DbConfig.Host, utils.GetSupabaseDbHost(project))) - }) -} - -func TestResolvePoolerConfigForFallback(t *testing.T) { - ref := apitest.RandomProjectRef() - - t.Run("uses linked pooler url with db password", func(t *testing.T) { - utils.Config.Db.Pooler.ConnectionString = "postgres://postgres." + ref + ":[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:6543/postgres" - viper.Set("DB_PASSWORD", "secret") - t.Cleanup(func() { - utils.Config.Db.Pooler.ConnectionString = "" - viper.Set("DB_PASSWORD", "") - }) - - config, err := ResolvePoolerConfigForFallback(context.Background(), ref) - - require.NoError(t, err) - assert.Equal(t, "aws-0-us-east-1.pooler.supabase.com", config.Host) - assert.Equal(t, uint16(5432), config.Port) - assert.Equal(t, "postgres."+ref, config.User) - assert.Equal(t, "secret", config.Password) - }) - - t.Run("falls back to api pooler config", func(t *testing.T) { - poolerURL := "postgres://postgres." + ref + ":[YOUR-PASSWORD]@aws-0-eu-west-1.pooler.supabase.com:6543/postgres" - utils.Config.Db.Pooler.ConnectionString = "" - viper.Set("DB_PASSWORD", "secret") - t.Cleanup(func() { viper.Set("DB_PASSWORD", "") }) - t.Cleanup(apitest.MockPlatformAPI(t)) - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + ref + "/config/database/pooler"). - Reply(http.StatusOK). - JSON([]api.SupavisorConfigResponseOutput{{ - DatabaseType: api.SupavisorConfigResponseOutputDatabaseTypePRIMARY, - ConnectionString: poolerURL, - }}) - - config, err := ResolvePoolerConfigForFallback(context.Background(), ref) - - require.NoError(t, err) - assert.Equal(t, "aws-0-eu-west-1.pooler.supabase.com", config.Host) - assert.Equal(t, uint16(5432), config.Port) - assert.Equal(t, "secret", config.Password) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("returns error when api pooler is unavailable", func(t *testing.T) { - utils.Config.Db.Pooler.ConnectionString = "" - t.Cleanup(apitest.MockPlatformAPI(t)) - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + ref + "/config/database/pooler"). - Reply(http.StatusOK). - JSON([]api.SupavisorConfigResponseOutput{}) - - _, err := ResolvePoolerConfigForFallback(context.Background(), ref) - - assert.Error(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} - -func TestPromptPassword(t *testing.T) { - t.Run("returns user input when provided", func(t *testing.T) { - r, w, err := os.Pipe() - require.NoError(t, err) - defer r.Close() - go func() { - defer w.Close() - _, err := w.Write([]byte("test-password")) - assert.Nil(t, err) - }() - - password := PromptPassword(r) - - assert.Equal(t, "test-password", password) - }) - - t.Run("generates password when input is empty", func(t *testing.T) { - r, w, err := os.Pipe() - require.NoError(t, err) - defer r.Close() - go func() { - defer w.Close() - _, err := w.Write([]byte("")) - assert.Nil(t, err) - }() - - password := PromptPassword(r) - - assert.Len(t, password, PASSWORD_LENGTH) - assert.NotEqual(t, "", password) - }) -} diff --git a/apps/cli-go/internal/utils/flags/project_ref.go b/apps/cli-go/internal/utils/flags/project_ref.go deleted file mode 100644 index 74d7c9a08e..0000000000 --- a/apps/cli-go/internal/utils/flags/project_ref.go +++ /dev/null @@ -1,80 +0,0 @@ -package flags - -import ( - "bytes" - "context" - "fmt" - "os" - - tea "github.com/charmbracelet/bubbletea" - "github.com/go-errors/errors" - "github.com/spf13/afero" - "github.com/spf13/viper" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/config" - "golang.org/x/term" -) - -var ProjectRef string - -func ParseProjectRef(ctx context.Context, fsys afero.Fs) error { - if err := LoadProjectRef(fsys); !errors.Is(err, utils.ErrNotLinked) { - return err - } - // Prompt as the last resort - if term.IsTerminal(int(os.Stdin.Fd())) { - return PromptProjectRef(ctx, "Select a project:") - } - return errors.New(utils.ErrNotLinked) -} - -func PromptProjectRef(ctx context.Context, title string, opts ...tea.ProgramOption) error { - resp, err := utils.GetSupabase().V1ListAllProjectsWithResponse(ctx) - if err != nil { - return errors.Errorf("failed to retrieve projects: %w", err) - } - if resp.JSON200 == nil { - return errors.New("Unexpected error retrieving projects: " + string(resp.Body)) - } - items := make([]utils.PromptItem, len(*resp.JSON200)) - for i, project := range *resp.JSON200 { - items[i] = utils.PromptItem{ - Summary: project.Id, - Details: fmt.Sprintf("name: %s, org: %s, region: %s", project.Name, project.OrganizationSlug, project.Region), - } - } - choice, err := utils.PromptChoice(ctx, title, items, opts...) - if err != nil { - return err - } - ProjectRef = choice.Summary - fmt.Fprintln(os.Stderr, "Selected project:", ProjectRef) - return nil -} - -func LoadProjectRef(fsys afero.Fs) error { - debuglogger := utils.GetDebugLogger() - // Flag takes highest precedence - if len(ProjectRef) > 0 { - fmt.Fprintf(debuglogger, "Loading project ref from flag: %s\n", ProjectRef) - return utils.AssertProjectRefIsValid(ProjectRef) - } - // Env var takes precedence over ref file - if ProjectRef = viper.GetString("PROJECT_ID"); len(ProjectRef) > 0 { - fmt.Fprintf(debuglogger, "Loading project ref from env var: %s\n", ProjectRef) - return utils.AssertProjectRefIsValid(ProjectRef) - } - if config.UsesStackState() { - return errors.New(utils.ErrNotLinked) - } - // Load from local file last - projectRefBytes, err := afero.ReadFile(fsys, utils.ProjectRefPath) - fmt.Fprintf(debuglogger, "Loading project ref from file: %s\n", utils.ProjectRefPath) - if errors.Is(err, os.ErrNotExist) { - return errors.New(utils.ErrNotLinked) - } else if err != nil { - return errors.Errorf("failed to load project ref: %w", err) - } - ProjectRef = string(bytes.TrimSpace(projectRefBytes)) - return utils.AssertProjectRefIsValid(ProjectRef) -} diff --git a/apps/cli-go/internal/utils/flags/project_ref_test.go b/apps/cli-go/internal/utils/flags/project_ref_test.go deleted file mode 100644 index b497a7b211..0000000000 --- a/apps/cli-go/internal/utils/flags/project_ref_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package flags - -import ( - "context" - "net/http" - "os" - "strings" - "testing" - - tea "github.com/charmbracelet/bubbletea" - "github.com/go-errors/errors" - "github.com/h2non/gock" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/testing/apitest" - "github.com/supabase/cli/internal/testing/fstest" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/config" -) - -func TestProjectRef(t *testing.T) { - t.Run("validates cmd flag", func(t *testing.T) { - ProjectRef = "invalid" - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Run test - err := ParseProjectRef(context.Background(), fsys) - // Check error - assert.Error(t, err, utils.ErrInvalidRef) - }) - - t.Run("loads from linked", func(t *testing.T) { - ProjectRef = "" - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Setup valid project ref - project := apitest.RandomProjectRef() - err := afero.WriteFile(fsys, utils.ProjectRefPath, []byte(project), 0644) - require.NoError(t, err) - // Run test - err = ParseProjectRef(context.Background(), fsys) - // Check error - assert.NoError(t, err) - }) - - t.Run("ignores the linked file in stack mode", func(t *testing.T) { - t.Setenv(config.StackStateEnv, "1") - ProjectRef = "" - fsys := afero.NewMemMapFs() - decoy := apitest.RandomProjectRef() - require.NoError(t, afero.WriteFile(fsys, utils.ProjectRefPath, []byte(decoy), 0644)) - // Run test - err := LoadProjectRef(fsys) - // Check error - assert.ErrorIs(t, err, utils.ErrNotLinked) - assert.Empty(t, ProjectRef) - }) - - t.Run("throws error on read failure", func(t *testing.T) { - ProjectRef = "" - // Setup in-memory fs - fsys := &fstest.OpenErrorFs{DenyPath: utils.ProjectRefPath} - // Run test - err := ParseProjectRef(context.Background(), fsys) - // Check error - assert.ErrorIs(t, err, os.ErrPermission) - }) - - t.Run("throws error if all fail", func(t *testing.T) { - ProjectRef = "" - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Run test - err := ParseProjectRef(context.Background(), fsys) - // Check error - assert.ErrorIs(t, err, utils.ErrNotLinked) - }) -} - -func TestProjectPrompt(t *testing.T) { - // Setup valid access token - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) - - t.Run("validates prompt input", func(t *testing.T) { - input := tea.WithInput(strings.NewReader("\r")) - // Setup mock api - defer gock.OffAll() - gock.New(utils.DefaultApiHost). - Get("/v1/projects"). - Reply(http.StatusOK). - JSON([]api.V1ProjectResponseOutput{{ - Id: "test-project", - Name: "My Project", - OrganizationSlug: "test-org", - }}) - // Run test - err := PromptProjectRef(context.Background(), "", input) - // Check error - assert.NoError(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on network failure", func(t *testing.T) { - errNetwork := errors.New("network error") - // Setup mock api - defer gock.OffAll() - gock.New(utils.DefaultApiHost). - Get("/v1/projects"). - ReplyError(errNetwork) - // Run test - err := PromptProjectRef(context.Background(), "") - // Check error - assert.ErrorIs(t, err, errNetwork) - }) - - t.Run("throws error on service unavailable", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New(utils.DefaultApiHost). - Get("/v1/projects"). - Reply(http.StatusServiceUnavailable) - // Run test - err := PromptProjectRef(context.Background(), "") - // Check error - assert.ErrorContains(t, err, "Unexpected error retrieving projects:") - }) -} diff --git a/apps/cli-go/internal/utils/git.go b/apps/cli-go/internal/utils/git.go deleted file mode 100644 index 328e7f94ae..0000000000 --- a/apps/cli-go/internal/utils/git.go +++ /dev/null @@ -1,27 +0,0 @@ -package utils - -import ( - "os" - - "github.com/go-git/go-git/v5" - "github.com/spf13/afero" -) - -func GetGitBranch(fsys afero.Fs) string { - return GetGitBranchOrDefault("main", fsys) -} - -func GetGitBranchOrDefault(def string, fsys afero.Fs) string { - head := os.Getenv("GITHUB_HEAD_REF") - if len(head) > 0 { - return head - } - if root, err := findGitRoot("."); err != nil { - return def - } else if repo, err := git.PlainOpen(root); err == nil { - if ref, err := repo.Head(); err == nil { - return ref.Name().Short() - } - } - return def -} diff --git a/apps/cli-go/internal/utils/identity_transport.go b/apps/cli-go/internal/utils/identity_transport.go deleted file mode 100644 index bcf01d97e7..0000000000 --- a/apps/cli-go/internal/utils/identity_transport.go +++ /dev/null @@ -1,21 +0,0 @@ -package utils - -import "net/http" - -const HeaderGotrueID = "X-Gotrue-Id" - -type identityTransport struct { - http.RoundTripper - onGotrueID *func(string) -} - -func (t *identityTransport) RoundTrip(req *http.Request) (*http.Response, error) { - resp, err := t.RoundTripper.RoundTrip(req) - if err != nil { - return resp, err - } - if id := resp.Header.Get(HeaderGotrueID); id != "" && t.onGotrueID != nil && *t.onGotrueID != nil { - (*t.onGotrueID)(id) - } - return resp, err -} diff --git a/apps/cli-go/internal/utils/identity_transport_test.go b/apps/cli-go/internal/utils/identity_transport_test.go deleted file mode 100644 index 314039d0a9..0000000000 --- a/apps/cli-go/internal/utils/identity_transport_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package utils - -import ( - "net/http" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestIdentityTransport_CapturesGotrueIdHeader(t *testing.T) { - var captured string - cb := func(id string) { captured = id } - transport := &identityTransport{ - RoundTripper: roundTripFunc(func(req *http.Request) (*http.Response, error) { - return &http.Response{ - StatusCode: 200, - Header: http.Header{"X-Gotrue-Id": []string{"user-abc-123"}}, - }, nil - }), - onGotrueID: &cb, - } - req, _ := http.NewRequest("GET", "https://api.supabase.io/v1/projects", nil) - resp, err := transport.RoundTrip(req) - assert.NoError(t, err) - assert.Equal(t, 200, resp.StatusCode) - assert.Equal(t, "user-abc-123", captured) -} - -func TestIdentityTransport_IgnoresWhenHeaderMissing(t *testing.T) { - var captured string - cb := func(id string) { captured = id } - transport := &identityTransport{ - RoundTripper: roundTripFunc(func(req *http.Request) (*http.Response, error) { - return &http.Response{ - StatusCode: 200, - Header: http.Header{}, - }, nil - }), - onGotrueID: &cb, - } - req, _ := http.NewRequest("GET", "https://api.supabase.io/v1/projects", nil) - _, err := transport.RoundTrip(req) - assert.NoError(t, err) - assert.Empty(t, captured) -} - -func TestIdentityTransport_NilCallbackDoesNotPanic(t *testing.T) { - transport := &identityTransport{ - RoundTripper: roundTripFunc(func(req *http.Request) (*http.Response, error) { - return &http.Response{ - StatusCode: 200, - Header: http.Header{"X-Gotrue-Id": []string{"user-abc-123"}}, - }, nil - }), - onGotrueID: nil, - } - req, _ := http.NewRequest("GET", "https://api.supabase.io/v1/projects", nil) - resp, err := transport.RoundTrip(req) - assert.NoError(t, err) - assert.Equal(t, 200, resp.StatusCode) -} - -func TestIdentityTransport_NilFuncBehindPointerDoesNotPanic(t *testing.T) { - var cb func(string) // nil func - transport := &identityTransport{ - RoundTripper: roundTripFunc(func(req *http.Request) (*http.Response, error) { - return &http.Response{ - StatusCode: 200, - Header: http.Header{"X-Gotrue-Id": []string{"user-abc-123"}}, - }, nil - }), - onGotrueID: &cb, - } - req, _ := http.NewRequest("GET", "https://api.supabase.io/v1/projects", nil) - resp, err := transport.RoundTrip(req) - assert.NoError(t, err) - assert.Equal(t, 200, resp.StatusCode) -} - -func TestIdentityTransport_InnerTransportError(t *testing.T) { - var captured string - cb := func(id string) { captured = id } - transport := &identityTransport{ - RoundTripper: roundTripFunc(func(req *http.Request) (*http.Response, error) { - return nil, assert.AnError - }), - onGotrueID: &cb, - } - req, _ := http.NewRequest("GET", "https://api.supabase.io/v1/projects", nil) - resp, err := transport.RoundTrip(req) - assert.Error(t, err) - assert.Nil(t, resp) - assert.Empty(t, captured) -} - -// roundTripFunc is a test helper to create inline RoundTrippers. -type roundTripFunc func(*http.Request) (*http.Response, error) - -func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { - return f(req) -} diff --git a/apps/cli-go/internal/utils/logger.go b/apps/cli-go/internal/utils/logger.go deleted file mode 100644 index 0cde1df38d..0000000000 --- a/apps/cli-go/internal/utils/logger.go +++ /dev/null @@ -1,15 +0,0 @@ -package utils - -import ( - "io" - "os" - - "github.com/spf13/viper" -) - -func GetDebugLogger() io.Writer { - if viper.GetBool("DEBUG") { - return os.Stderr - } - return io.Discard -} diff --git a/apps/cli-go/internal/utils/misc.go b/apps/cli-go/internal/utils/misc.go deleted file mode 100644 index 186573146b..0000000000 --- a/apps/cli-go/internal/utils/misc.go +++ /dev/null @@ -1,318 +0,0 @@ -package utils - -import ( - "context" - _ "embed" - "fmt" - "net" - "os" - "path/filepath" - "regexp" - "time" - - "github.com/containerd/errdefs" - "github.com/docker/docker/client" - "github.com/go-errors/errors" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing/format/gitignore" - "github.com/spf13/afero" - "github.com/spf13/viper" - "github.com/supabase/cli/internal/utils/agent" - "github.com/supabase/cli/pkg/migration" - "golang.org/x/term" -) - -// Assigned using `-ldflags` https://stackoverflow.com/q/11354518 -var ( - Version string - SentryDsn string - PostHogAPIKey string - PostHogEndpoint string -) - -func ShortContainerImageName(imageName string) string { - matches := ImageNamePattern.FindStringSubmatch(imageName) - if len(matches) < 2 { - return imageName - } - return matches[1] -} - -const SuggestDebugFlag = "Try rerunning the command with --debug to troubleshoot the error." - -const claudeCodeHint = `` - -func SuggestClaudePlugin() string { - if !agent.IsClaudeCode() { - return "" - } - // Suppress the hint when stdout is non-interactive (redirected to a file - // or piped). Without this guard, captured output could be contaminated by - // the trailer if anything ever leaks the tag onto stdout, and the - // install-prompt UX is only meaningful in an interactive shell anyway. - if !term.IsTerminal(int(os.Stdout.Fd())) { //nolint:gosec // G115: stdout fd is a small int on supported platforms - return "" - } - return claudeCodeHint -} - -var ( - CmdSuggestion string - CurrentDirAbs string - - // pg_dumpall --globals-only --no-role-passwords --dbname $DB_URL \ - // | sed '/^CREATE ROLE postgres;/d' \ - // | sed '/^ALTER ROLE postgres WITH /d' \ - // | sed "/^ALTER ROLE .* WITH .* LOGIN /s/;$/ PASSWORD 'postgres';/" - //go:embed templates/globals.sql - GlobalsSql string - - ProjectRefPattern = regexp.MustCompile(`^[a-z]{20}$`) - UUIDPattern = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`) - ProjectHostPattern = regexp.MustCompile(`^(db\.)([a-z]{20})\.supabase\.(co|red)$`) - BranchNamePattern = regexp.MustCompile(`[[:word:]-]+`) - FuncSlugPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]*$`) - ImageNamePattern = regexp.MustCompile(`\/(.*):`) - - // These schemas are ignored from db diff and db dump - PgSchemas = migration.InternalSchemas[:2] - InternalSchemas = migration.InternalSchemas - - SupabaseDirPath = "supabase" - ConfigPath = filepath.Join(SupabaseDirPath, "config.toml") - GitIgnorePath = filepath.Join(SupabaseDirPath, ".gitignore") - TempDir = filepath.Join(SupabaseDirPath, ".temp") - ImportMapsDir = filepath.Join(TempDir, "import_maps") - ProjectRefPath = filepath.Join(TempDir, "project-ref") - PoolerUrlPath = filepath.Join(TempDir, "pooler-url") - PostgresVersionPath = filepath.Join(TempDir, "postgres-version") - GotrueVersionPath = filepath.Join(TempDir, "gotrue-version") - RestVersionPath = filepath.Join(TempDir, "rest-version") - StorageVersionPath = filepath.Join(TempDir, "storage-version") - StorageMigrationPath = filepath.Join(TempDir, "storage-migration") - StudioVersionPath = filepath.Join(TempDir, "studio-version") - PgmetaVersionPath = filepath.Join(TempDir, "pgmeta-version") - PoolerVersionPath = filepath.Join(TempDir, "pooler-version") - RealtimeVersionPath = filepath.Join(TempDir, "realtime-version") - CliVersionPath = filepath.Join(TempDir, "cli-latest") - CurrBranchPath = filepath.Join(SupabaseDirPath, ".branches", "_current_branch") - // DeclarativeDir is the canonical location for pg-delta declarative schema - // files generated or synced by `supabase db schema declarative` commands. - DeclarativeDir = filepath.Join(SupabaseDirPath, "schemas") - ClusterDir = filepath.Join(SupabaseDirPath, "cluster") - SchemasDir = filepath.Join(SupabaseDirPath, "schemas") - MigrationsDir = filepath.Join(SupabaseDirPath, "migrations") - FunctionsDir = filepath.Join(SupabaseDirPath, "functions") - SnippetsDir = filepath.Join(SupabaseDirPath, "snippets") - FallbackImportMapPath = filepath.Join(FunctionsDir, "import_map.json") - FallbackEnvFilePath = filepath.Join(FunctionsDir, ".env") - DbTestsDir = filepath.Join(SupabaseDirPath, "tests") - CustomRolesPath = filepath.Join(SupabaseDirPath, "roles.sql") - - ErrNotLinked = errors.Errorf("Cannot find project ref. Have you run %s?", Aqua("supabase link")) - ErrInvalidRef = errors.New("Invalid project ref format. Must be like `abcdefghijklmnopqrst`.") - ErrInvalidSlug = errors.New("Invalid Function name. Must start with at least one letter, and only include alphanumeric characters, underscores, and hyphens. (^[A-Za-z][A-Za-z0-9_-]*$)") - ErrNotRunning = errors.Errorf("%s is not running.", Aqua("supabase start")) -) - -func GetDeclarativeDir() string { - if Config.Experimental.PgDelta != nil && len(Config.Experimental.PgDelta.DeclarativeSchemaPath) > 0 { - return Config.Experimental.PgDelta.DeclarativeSchemaPath - } - return DeclarativeDir -} - -func IsPgDeltaEnabled() bool { - return Config.Experimental.PgDelta != nil && Config.Experimental.PgDelta.Enabled -} - -func GetCurrentTimestamp() string { - // Magic number: https://stackoverflow.com/q/45160822. - return GetVersionTimestamp(time.Now()) -} - -// GetVersionTimestamp formats t as a migration version (UTC `YYYYMMDDHHMMSS`). -// Callers that write several ordered migration files in one pass add real time -// offsets to a shared base rather than incrementing the formatted string. -func GetVersionTimestamp(t time.Time) string { - return t.UTC().Format(layoutVersion) -} - -func GetCurrentBranchFS(fsys afero.Fs) (string, error) { - branch, err := afero.ReadFile(fsys, CurrBranchPath) - if err != nil { - return "", errors.Errorf("failed to load current branch: %w", err) - } - - return string(branch), nil -} - -func AssertSupabaseDbIsRunning() error { - return AssertServiceIsRunning(context.Background(), DbId) -} - -func AssertServiceIsRunning(ctx context.Context, containerId string) error { - if _, err := Docker.ContainerInspect(ctx, containerId); err != nil { - if errdefs.IsNotFound(err) { - return errors.New(ErrNotRunning) - } - if client.IsErrConnectionFailed(err) { - CmdSuggestion = suggestDockerInstall - } - return errors.Errorf("failed to inspect service: %w", err) - } - return nil -} - -func IsGitRepo() bool { - _, err := findGitRoot(".") - return err == nil -} - -func IsGitIgnored(fp ...string) (bool, error) { - root, err := findGitRoot(".") - if err != nil { - return false, err - } - repo, err := git.PlainOpen(root) - if err != nil { - return false, err - } - wt, err := repo.Worktree() - if err != nil { - return false, err - } - ps, err := gitignore.ReadPatterns(wt.Filesystem, nil) - if err != nil { - return false, err - } - m := gitignore.NewMatcher(ps) - return m.Match(fp, false), nil -} - -func findGitRoot(path string) (string, error) { - cwd, err := filepath.Abs(path) - if err != nil { - return "", err - } - for { - if _, err := os.Stat(filepath.Join(cwd, git.GitDirName)); err == nil { - return cwd, nil - } else if !errors.Is(err, os.ErrNotExist) { - return "", err - } - if parent := filepath.Dir(cwd); parent != cwd { - cwd = parent - } else { - return "", git.ErrRepositoryNotExists - } - } -} - -// If the `os.Getwd()` is within a supabase project, this will return -// the root of the given project as the current working directory. -// Otherwise, the `os.Getwd()` is kept as is. -func getProjectRoot(absPath string, fsys afero.Fs) string { - for cwd := absPath; ; cwd = filepath.Dir(cwd) { - path := filepath.Join(cwd, ConfigPath) - // Treat all errors as file not exists - if isSupaProj, err := afero.Exists(fsys, path); isSupaProj { - return cwd - } else if err != nil && !errors.Is(err, os.ErrNotExist) { - logger := GetDebugLogger() - fmt.Fprintln(logger, err) - } - if isRootDirectory(cwd) { - break - } - } - return absPath -} - -func isRootDirectory(cleanPath string) bool { - // A cleaned path only ends with separator if it is root - return os.IsPathSeparator(cleanPath[len(cleanPath)-1]) -} - -func ChangeWorkDir(fsys afero.Fs) error { - // Track the original workdir before changing to project root - if !filepath.IsAbs(CurrentDirAbs) { - var err error - if CurrentDirAbs, err = os.Getwd(); err != nil { - return errors.Errorf("failed to get current directory: %w", err) - } - } - workdir := viper.GetString("WORKDIR") - if len(workdir) == 0 { - workdir = getProjectRoot(CurrentDirAbs, fsys) - } - if err := os.Chdir(workdir); err != nil { - return errors.Errorf("failed to change workdir: %w", err) - } - if cwd, err := os.Getwd(); err == nil && cwd != CurrentDirAbs { - fmt.Fprintln(os.Stderr, "Using workdir", Bold(workdir)) - } - return nil -} - -func IsBranchNameReserved(branch string) bool { - switch branch { - case "_current_branch", "main", "postgres", "template0", "template1": - return true - default: - return false - } -} - -func MkdirIfNotExist(path string) error { - return MkdirIfNotExistFS(afero.NewOsFs(), path) -} - -func MkdirIfNotExistFS(fsys afero.Fs, path string) error { - if err := fsys.MkdirAll(path, 0755); err != nil && !errors.Is(err, os.ErrExist) { - return errors.Errorf("failed to mkdir: %w", err) - } - - return nil -} - -func WriteFile(path string, contents []byte, fsys afero.Fs) error { - if err := MkdirIfNotExistFS(fsys, filepath.Dir(path)); err != nil { - return err - } - if err := afero.WriteFile(fsys, path, contents, 0644); err != nil { - return errors.Errorf("failed to write file: %w", err) - } - return nil -} - -func AssertProjectRefIsValid(projectRef string) error { - if !ProjectRefPattern.MatchString(projectRef) { - return errors.New(ErrInvalidRef) - } - return nil -} - -func ValidateFunctionSlug(slug string) error { - if !FuncSlugPattern.MatchString(slug) { - return errors.New(ErrInvalidSlug) - } - - return nil -} - -func GetHostname() string { - // Overrides the host for local service connections. Useful when - // running inside a dev container when the Docker host is not - // 127.0.0.1 (container's own loopback). - if h := os.Getenv("SUPABASE_SERVICES_HOSTNAME"); h != "" { - return h - } - host := Docker.DaemonHost() - if parsed, err := client.ParseHostURL(host); err == nil && parsed.Scheme == "tcp" { - if host, _, err := net.SplitHostPort(parsed.Host); err == nil { - return host - } - } - return "127.0.0.1" -} diff --git a/apps/cli-go/internal/utils/misc_test.go b/apps/cli-go/internal/utils/misc_test.go deleted file mode 100644 index e8e075201b..0000000000 --- a/apps/cli-go/internal/utils/misc_test.go +++ /dev/null @@ -1,217 +0,0 @@ -package utils - -import ( - "io/fs" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/pkg/config" -) - -type MockFs struct { - afero.MemMapFs - DenyPath string -} - -func (m *MockFs) Stat(name string) (fs.FileInfo, error) { - if strings.HasPrefix(name, m.DenyPath) { - return nil, fs.ErrPermission - } - return m.MemMapFs.Stat(name) -} - -func TestProjectRoot(t *testing.T) { - root := string(filepath.Separator) - - t.Run("stops at root dir", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - _, err := fsys.Create(filepath.Join(root, ConfigPath)) - require.NoError(t, err) - // Run test - cwd := filepath.Join(root, "home", "user", "project") - path := getProjectRoot(cwd, fsys) - // Check error - assert.Equal(t, root, path) - }) - - t.Run("stops at closest parent", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - _, err := fsys.Create(filepath.Join(root, "supabase", ConfigPath)) - require.NoError(t, err) - // Run test - cwd := filepath.Join(root, "supabase", "supabase", "functions") - path := getProjectRoot(cwd, fsys) - // Check error - assert.Equal(t, filepath.Join(root, "supabase"), path) - }) - - t.Run("ignores error on config not found", func(t *testing.T) { - cwd, err := os.Getwd() - require.NoError(t, err) - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Run test - path := getProjectRoot(cwd, fsys) - // Check error - assert.NoError(t, err) - assert.Equal(t, cwd, path) - }) - - t.Run("ignores error if path is not directory", func(t *testing.T) { - cwd, err := os.Getwd() - require.NoError(t, err) - // Setup in-memory fs - fsys := &MockFs{DenyPath: filepath.Join(cwd, "supabase")} - // Run test - path := getProjectRoot(cwd, fsys) - // Check error - assert.NoError(t, err) - assert.Equal(t, cwd, path) - }) -} - -func TestShortContainerImageName(t *testing.T) { - t.Run("extracts short name from image", func(t *testing.T) { - input := "registry.supabase.com/postgres:15.1.0.99" - expected := "postgres" - - result := ShortContainerImageName(input) - - assert.Equal(t, expected, result) - }) -} - -func TestIsBranchNameReserved(t *testing.T) { - t.Run("identifies reserved names", func(t *testing.T) { - reserved := []string{"_current_branch", "main"} - for _, name := range reserved { - assert.True(t, IsBranchNameReserved(name), "Expected %s to be reserved", name) - } - }) - - t.Run("allows custom names", func(t *testing.T) { - allowed := []string{"my-feature", "test-branch-123"} - for _, name := range allowed { - assert.False(t, IsBranchNameReserved(name), "Expected %s to be allowed", name) - } - }) -} - -func TestValidateFunctionSlug(t *testing.T) { - t.Run("validates correct slugs", func(t *testing.T) { - valid := []string{ - "my-function", - "MyFunction", - "function_1", - "a123", - } - for _, slug := range valid { - err := ValidateFunctionSlug(slug) - assert.NoError(t, err, "Expected %s to be valid", slug) - } - }) - - t.Run("rejects invalid slugs", func(t *testing.T) { - invalid := []string{ - "1function", - "my function", - "function!", - "", - "-function", - "_function", - } - for _, slug := range invalid { - err := ValidateFunctionSlug(slug) - assert.ErrorIs(t, err, ErrInvalidSlug, "Expected %s to be invalid", slug) - } - }) -} - -func TestAssertProjectRefIsValid(t *testing.T) { - t.Run("validates correct refs", func(t *testing.T) { - validRef := "abcdefghijklmnopqrst" - err := AssertProjectRefIsValid(validRef) - assert.NoError(t, err) - }) - - t.Run("rejects invalid refs", func(t *testing.T) { - invalid := []string{ - "tooshort", - "toolongabcdefghijklmnopqrst", - "UPPERCASE", - "special-chars", - "123", - "", - } - for _, ref := range invalid { - err := AssertProjectRefIsValid(ref) - assert.ErrorIs(t, err, ErrInvalidRef, "Expected %s to be invalid", ref) - } - }) -} - -func TestGetHostname(t *testing.T) { - t.Run("returns SUPABASE_SERVICES_HOSTNAME when set", func(t *testing.T) { - t.Setenv("SUPABASE_SERVICES_HOSTNAME", "host.docker.internal") - assert.Equal(t, "host.docker.internal", GetHostname()) - }) - - t.Run("returns 127.0.0.1 when SUPABASE_SERVICES_HOSTNAME is not set", func(t *testing.T) { - t.Setenv("SUPABASE_SERVICES_HOSTNAME", "") - assert.Equal(t, "127.0.0.1", GetHostname()) - }) -} - -func TestWriteFile(t *testing.T) { - t.Run("writes file with directories", func(t *testing.T) { - fsys := afero.NewMemMapFs() - path := filepath.Join("deep", "nested", "dir", "file.txt") - content := []byte("test content") - - err := WriteFile(path, content, fsys) - - assert.NoError(t, err) - - written, err := afero.ReadFile(fsys, path) - assert.NoError(t, err) - assert.Equal(t, content, written) - }) - - t.Run("overwrites existing file", func(t *testing.T) { - fsys := afero.NewMemMapFs() - path := "test.txt" - original := []byte("original") - updated := []byte("updated") - require.NoError(t, afero.WriteFile(fsys, path, original, 0644)) - - err := WriteFile(path, updated, fsys) - - assert.NoError(t, err) - written, err := afero.ReadFile(fsys, path) - assert.NoError(t, err) - assert.Equal(t, updated, written) - }) -} - -func TestGetDeclarativeDir(t *testing.T) { - t.Run("uses configured pgdelta path", func(t *testing.T) { - Config.Experimental.PgDelta = &config.PgDeltaConfig{ - DeclarativeSchemaPath: filepath.Join(SupabaseDirPath, "db", "decl"), - } - - assert.Equal(t, filepath.Join(SupabaseDirPath, "db", "decl"), GetDeclarativeDir()) - }) - - t.Run("falls back to default declarative dir", func(t *testing.T) { - Config.Experimental.PgDelta = nil - - assert.Equal(t, DeclarativeDir, GetDeclarativeDir()) - }) -} diff --git a/apps/cli-go/internal/utils/output.go b/apps/cli-go/internal/utils/output.go deleted file mode 100644 index df879fb427..0000000000 --- a/apps/cli-go/internal/utils/output.go +++ /dev/null @@ -1,123 +0,0 @@ -package utils - -import ( - "encoding/json" - "fmt" - "io" - "strings" - - "github.com/BurntSushi/toml" - "github.com/charmbracelet/glamour" - "github.com/charmbracelet/glamour/styles" - "github.com/go-errors/errors" - "github.com/go-viper/mapstructure/v2" - "github.com/joho/godotenv" - "github.com/spf13/viper" - "gopkg.in/yaml.v3" -) - -const ( - OutputEnv = "env" - OutputJson = "json" - OutputPretty = "pretty" - OutputToml = "toml" - OutputYaml = "yaml" - - // OutputMetadata is used with certain SSO commands only. - OutputMetadata = "metadata" -) - -var OutputFormat = EnumFlag{ - Allowed: []string{ - OutputEnv, - OutputPretty, - OutputJson, - OutputToml, - OutputYaml, - }, - Value: OutputPretty, -} - -var ErrEnvNotSupported = errors.New("--output env flag is not supported") - -func EncodeOutput(format string, w io.Writer, value any) error { - switch format { - case OutputEnv: - mapvalue, err := ToEnvMap(value) - if err != nil { - return err - } - - out, err := godotenv.Marshal(mapvalue) - if err != nil { - return errors.Errorf("failed to marshal env map: %w", err) - } - - if _, err := fmt.Fprintln(w, out); err != nil { - return errors.Errorf("failed to write encoded output: %w", err) - } - - case OutputJson: - enc := json.NewEncoder(w) - enc.SetIndent("", " ") - - if err := enc.Encode(value); err != nil { - return errors.Errorf("failed to output json: %w", err) - } - - case OutputYaml: - enc := yaml.NewEncoder(w) - if err := enc.Encode(value); err != nil { - return errors.Errorf("failed to output yaml: %w", err) - } - - case OutputToml: - enc := toml.NewEncoder(w) - if err := enc.Encode(value); err != nil { - return errors.Errorf("failed to output toml: %w", err) - } - - default: - return errors.Errorf("Unsupported output encoding %q", format) - } - return nil -} - -func ToEnvMap(value any) (map[string]string, error) { - var result map[string]any - if dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ - TagName: "json", - Result: &result, - }); err != nil { - return nil, errors.Errorf("failed to init decoder: %w", err) - } else if err := dec.Decode(value); err != nil { - return nil, errors.Errorf("failed to decode env: %w", err) - } - v := viper.New() - if err := v.MergeConfigMap(result); err != nil { - return nil, errors.Errorf("failed to merge env: %w", err) - } - keys := v.AllKeys() - mapvalue := make(map[string]string, len(keys)) - for _, k := range keys { - name := strings.ToUpper(strings.ReplaceAll(k, ".", "_")) - mapvalue[name] = v.GetString(k) - } - return mapvalue, nil -} - -func RenderTable(markdown string) error { - r, err := glamour.NewTermRenderer( - glamour.WithStandardStyle(styles.AsciiStyle), - glamour.WithWordWrap(-1), - ) - if err != nil { - return errors.Errorf("failed to initialise terminal renderer: %w", err) - } - out, err := r.Render(markdown) - if err != nil { - return errors.Errorf("failed to render markdown: %w", err) - } - fmt.Print(out) - return nil -} diff --git a/apps/cli-go/internal/utils/output_test.go b/apps/cli-go/internal/utils/output_test.go deleted file mode 100644 index a04be2536b..0000000000 --- a/apps/cli-go/internal/utils/output_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package utils - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestEncodeOutput(t *testing.T) { - t.Run("encodes flat env", func(t *testing.T) { - input := map[string]string{ - "DATABASE_URL": "postgres://user:pass@host:5432/db", - "API_KEY": "secret-key", - } - var buf bytes.Buffer - err := EncodeOutput(OutputEnv, &buf, input) - assert.NoError(t, err) - expected := "API_KEY=\"secret-key\"\nDATABASE_URL=\"postgres://user:pass@host:5432/db\"\n" - assert.Equal(t, expected, buf.String()) - }) - - t.Run("encodes nested env", func(t *testing.T) { - input := map[string]any{ - "FOO": map[string]any{ - "BAR": 123, - }, - } - var buf bytes.Buffer - err := EncodeOutput(OutputEnv, &buf, input) - assert.NoError(t, err) - assert.Equal(t, "FOO_BAR=123\n", buf.String()) - }) - - t.Run("encodes json format", func(t *testing.T) { - input := map[string]any{ - "name": "test", - "config": map[string]any{ - "port": 5432, - "enabled": true, - }, - } - var buf bytes.Buffer - err := EncodeOutput(OutputJson, &buf, input) - assert.NoError(t, err) - expected := `{ - "config": { - "enabled": true, - "port": 5432 - }, - "name": "test" -} -` - assert.Equal(t, expected, buf.String()) - }) - - t.Run("encodes yaml format", func(t *testing.T) { - input := map[string]any{ - "name": "test", - "config": map[string]any{ - "port": 5432, - "enabled": true, - }, - } - var buf bytes.Buffer - err := EncodeOutput(OutputYaml, &buf, input) - assert.NoError(t, err) - expected := `config: - enabled: true - port: 5432 -name: test -` - assert.Equal(t, expected, buf.String()) - }) - - t.Run("encodes toml format", func(t *testing.T) { - input := map[string]any{ - "name": "test", - "config": map[string]any{ - "port": 5432, - "enabled": true, - }, - } - var buf bytes.Buffer - err := EncodeOutput(OutputToml, &buf, input) - assert.NoError(t, err) - expected := `name = "test" - -[config] - enabled = true - port = 5432 -` - assert.Equal(t, expected, buf.String()) - }) - - t.Run("fails with unsupported format", func(t *testing.T) { - var buf bytes.Buffer - err := EncodeOutput("invalid", &buf, nil) - assert.ErrorContains(t, err, `Unsupported output encoding "invalid"`) - }) - - t.Run("handles complex nested structures", func(t *testing.T) { - input := map[string]any{ - "database": map[string]any{ - "connections": []map[string]any{ - { - "host": "localhost", - "port": 5432, - }, - { - "host": "remote", - "port": 6543, - }, - }, - "settings": map[string]any{ - "max_connections": 100, - "ssl_enabled": true, - }, - }, - } - var buf bytes.Buffer - err := EncodeOutput(OutputJson, &buf, input) - require.NoError(t, err) - expected := `{ - "database": { - "connections": [ - { - "host": "localhost", - "port": 5432 - }, - { - "host": "remote", - "port": 6543 - } - ], - "settings": { - "max_connections": 100, - "ssl_enabled": true - } - } -} -` - assert.Equal(t, expected, buf.String()) - }) -} diff --git a/apps/cli-go/internal/utils/plan_gate.go b/apps/cli-go/internal/utils/plan_gate.go deleted file mode 100644 index c666725557..0000000000 --- a/apps/cli-go/internal/utils/plan_gate.go +++ /dev/null @@ -1,97 +0,0 @@ -package utils - -import ( - "context" - "encoding/json" - "fmt" - "strings" - - "github.com/supabase/cli/pkg/api" -) - -func GetOrgSlugFromProjectRef(ctx context.Context, projectRef string) (string, error) { - resp, err := GetSupabase().V1GetProjectWithResponse(ctx, projectRef) - if err != nil { - return "", fmt.Errorf("failed to get project: %w", err) - } - if resp.JSON200 == nil { - return "", fmt.Errorf("unexpected get project status %d: %s", resp.StatusCode(), string(resp.Body)) - } - return resp.JSON200.OrganizationSlug, nil -} - -func GetOrgBillingURL(orgSlug string) string { - return fmt.Sprintf("%s/org/%s/billing", GetSupabaseDashboardURL(), orgSlug) -} - -func orgSlugFromBillingURL(billingURL string) string { - const marker = "/org/" - start := strings.Index(billingURL, marker) - if start < 0 { - return "" - } - rest := billingURL[start+len(marker):] - if end := strings.Index(rest, "/"); end >= 0 { - return rest[:end] - } - return rest -} - -func parsePlanGateError(body []byte) (feature, upgradeURL string, ok bool) { - var envelope api.PlanGateErrorBody - if err := json.Unmarshal(body, &envelope); err != nil || envelope.Error == nil { - return "", "", false - } - if envelope.Error.Code != api.EntitlementRequired || envelope.Error.Feature == "" { - return "", "", false - } - if envelope.Error.UpgradeUrl == nil || *envelope.Error.UpgradeUrl == "" { - return "", "", false - } - return envelope.Error.Feature, *envelope.Error.UpgradeUrl, true -} - -func setUpgradeSuggestion(billingURL string) { - CmdSuggestion = fmt.Sprintf("Your organization does not have access to this feature. Upgrade your plan: %s", Bold(billingURL)) -} - -// SuggestUpgradeOnError checks whether a failed API response is a plan gate and -// sets CmdSuggestion with the billing URL when it is. The entitlement_required -// envelope on the response body is authoritative and needs no network calls; -// when absent, a non-empty featureKey falls back to the entitlements lookup. -// An empty featureKey disables the fallback (envelope-only sites). Returns the -// effective feature and org slug for telemetry, and whether a suggestion was -// shown. Only fires on 4xx. -func SuggestUpgradeOnError(ctx context.Context, projectRef, featureKey string, statusCode int, body []byte) (gatedFeature, orgSlug string, isGated bool) { - if statusCode < 400 || statusCode >= 500 { - return "", "", false - } - - if feature, upgradeURL, ok := parsePlanGateError(body); ok { - setUpgradeSuggestion(upgradeURL) - return feature, orgSlugFromBillingURL(upgradeURL), true - } - - if featureKey == "" { - return "", "", false - } - - slug, err := GetOrgSlugFromProjectRef(ctx, projectRef) - if err != nil { - return "", "", false - } - - resp, err := GetSupabase().V1GetOrganizationEntitlementsWithResponse(ctx, slug) - if err != nil || resp.JSON200 == nil { - return "", slug, false - } - - for _, e := range resp.JSON200.Entitlements { - if string(e.Feature.Key) == featureKey && !e.HasAccess { - setUpgradeSuggestion(GetOrgBillingURL(slug)) - return featureKey, slug, true - } - } - - return "", slug, false -} diff --git a/apps/cli-go/internal/utils/plan_gate_test.go b/apps/cli-go/internal/utils/plan_gate_test.go deleted file mode 100644 index 5f85d5076f..0000000000 --- a/apps/cli-go/internal/utils/plan_gate_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package utils - -import ( - "context" - "net/http" - "testing" - - "github.com/h2non/gock" - "github.com/stretchr/testify/assert" - "github.com/supabase/cli/internal/testing/apitest" -) - -var planGateProjectJSON = map[string]interface{}{ - "ref": "test-ref", - "organization_slug": "my-org", - "name": "test", - "region": "us-east-1", - "created_at": "2024-01-01T00:00:00Z", - "status": "ACTIVE_HEALTHY", - "database": map[string]interface{}{"host": "db.example.supabase.co", "version": "15.1.0.117"}, -} - -func TestGetOrgSlugFromProjectRef(t *testing.T) { - ref := apitest.RandomProjectRef() - - t.Run("returns org slug on success", func(t *testing.T) { - t.Cleanup(apitest.MockPlatformAPI(t)) - gock.New(DefaultApiHost). - Get("/v1/projects/" + ref). - Reply(http.StatusOK). - JSON(planGateProjectJSON) - slug, err := GetOrgSlugFromProjectRef(context.Background(), ref) - assert.NoError(t, err) - assert.Equal(t, "my-org", slug) - }) - - t.Run("returns error on not found", func(t *testing.T) { - t.Cleanup(apitest.MockPlatformAPI(t)) - gock.New(DefaultApiHost). - Get("/v1/projects/" + ref). - Reply(http.StatusNotFound) - _, err := GetOrgSlugFromProjectRef(context.Background(), ref) - assert.ErrorContains(t, err, "unexpected get project status 404") - }) - - t.Run("returns error on network failure", func(t *testing.T) { - t.Cleanup(apitest.MockPlatformAPI(t)) - gock.New(DefaultApiHost). - Get("/v1/projects/" + ref). - ReplyError(assert.AnError) - _, err := GetOrgSlugFromProjectRef(context.Background(), ref) - assert.ErrorContains(t, err, "failed to get project") - }) -} - -func TestGetOrgBillingURL(t *testing.T) { - url := GetOrgBillingURL("my-org") - assert.Equal(t, GetSupabaseDashboardURL()+"/org/my-org/billing", url) -} - -func entitlementsJSON(featureKey string, hasAccess bool) map[string]interface{} { - return map[string]interface{}{ - "entitlements": []map[string]interface{}{ - { - "feature": map[string]interface{}{"key": featureKey, "type": "numeric"}, - "hasAccess": hasAccess, - "type": "numeric", - "config": map[string]interface{}{"enabled": hasAccess, "value": 0, "unlimited": false, "unit": "count"}, - }, - }, - } -} - -// mockEntitlementsCheck sets up gock mocks for project lookup + entitlements. -func mockEntitlementsCheck(ref string, featureKey string, hasAccess bool) { - gock.New(DefaultApiHost). - Get("/v1/projects/" + ref). - Reply(http.StatusOK). - JSON(planGateProjectJSON) - gock.New(DefaultApiHost). - Get("/v1/organizations/my-org/entitlements"). - Reply(http.StatusOK). - JSON(entitlementsJSON(featureKey, hasAccess)) -} - -func TestSuggestUpgradeOnError(t *testing.T) { - ref := apitest.RandomProjectRef() - - t.Run("sets suggestion on 402 with gated feature", func(t *testing.T) { - t.Cleanup(apitest.MockPlatformAPI(t)) - t.Cleanup(func() { CmdSuggestion = "" }) - mockEntitlementsCheck(ref, "branching_limit", false) - feature, slug, got := SuggestUpgradeOnError(context.Background(), ref, "branching_limit", http.StatusPaymentRequired, nil) - assert.True(t, got) - assert.Equal(t, "branching_limit", feature) - assert.Equal(t, "my-org", slug) - assert.Contains(t, CmdSuggestion, "/org/my-org/billing") - assert.Contains(t, CmdSuggestion, "does not have access") - }) - - t.Run("sets suggestion on 400 with gated feature", func(t *testing.T) { - t.Cleanup(apitest.MockPlatformAPI(t)) - t.Cleanup(func() { CmdSuggestion = "" }) - mockEntitlementsCheck(ref, "vanity_subdomain", false) - feature, slug, got := SuggestUpgradeOnError(context.Background(), ref, "vanity_subdomain", http.StatusBadRequest, nil) - assert.True(t, got) - assert.Equal(t, "vanity_subdomain", feature) - assert.Equal(t, "my-org", slug) - assert.Contains(t, CmdSuggestion, "/org/my-org/billing") - assert.Contains(t, CmdSuggestion, "does not have access") - }) - - t.Run("sets suggestion on 404 with gated feature", func(t *testing.T) { - t.Cleanup(apitest.MockPlatformAPI(t)) - t.Cleanup(func() { CmdSuggestion = "" }) - mockEntitlementsCheck(ref, "auth.saml_2", false) - _, slug, got := SuggestUpgradeOnError(context.Background(), ref, "auth.saml_2", http.StatusNotFound, nil) - assert.True(t, got) - assert.Equal(t, "my-org", slug) - assert.Contains(t, CmdSuggestion, "/org/my-org/billing") - }) - - t.Run("no suggestion when entitlements lookup fails", func(t *testing.T) { - t.Cleanup(apitest.MockPlatformAPI(t)) - t.Cleanup(func() { CmdSuggestion = "" }) - gock.New(DefaultApiHost). - Get("/v1/projects/" + ref). - Reply(http.StatusOK). - JSON(planGateProjectJSON) - gock.New(DefaultApiHost). - Get("/v1/organizations/my-org/entitlements"). - Reply(http.StatusInternalServerError) - _, slug, got := SuggestUpgradeOnError(context.Background(), ref, "branching_limit", http.StatusPaymentRequired, nil) - assert.False(t, got) - assert.Equal(t, "my-org", slug) - assert.Empty(t, CmdSuggestion) - }) - - t.Run("no suggestion when project lookup fails", func(t *testing.T) { - t.Cleanup(apitest.MockPlatformAPI(t)) - t.Cleanup(func() { CmdSuggestion = "" }) - gock.New(DefaultApiHost). - Get("/v1/projects/" + ref). - Reply(http.StatusNotFound) - _, slug, got := SuggestUpgradeOnError(context.Background(), ref, "branching_limit", http.StatusPaymentRequired, nil) - assert.False(t, got) - assert.Empty(t, slug) - assert.Empty(t, CmdSuggestion) - }) - - t.Run("no suggestion when feature has access", func(t *testing.T) { - t.Cleanup(apitest.MockPlatformAPI(t)) - t.Cleanup(func() { CmdSuggestion = "" }) - mockEntitlementsCheck(ref, "branching_limit", true) - _, slug, got := SuggestUpgradeOnError(context.Background(), ref, "branching_limit", http.StatusPaymentRequired, nil) - assert.False(t, got) - assert.Equal(t, "my-org", slug) - assert.Empty(t, CmdSuggestion) - }) - - t.Run("skips on 503 server error", func(t *testing.T) { - CmdSuggestion = "" - _, _, got := SuggestUpgradeOnError(context.Background(), ref, "branching_limit", http.StatusServiceUnavailable, nil) - assert.False(t, got) - assert.Empty(t, CmdSuggestion) - }) - - t.Run("skips on 200", func(t *testing.T) { - CmdSuggestion = "" - _, _, got := SuggestUpgradeOnError(context.Background(), ref, "branching_limit", http.StatusOK, nil) - assert.False(t, got) - assert.Empty(t, CmdSuggestion) - }) - - t.Run("skips on 201", func(t *testing.T) { - CmdSuggestion = "" - _, _, got := SuggestUpgradeOnError(context.Background(), ref, "branching_limit", http.StatusCreated, nil) - assert.False(t, got) - assert.Empty(t, CmdSuggestion) - }) -} - -func TestSuggestUpgradeEnvelope(t *testing.T) { - envelope := `{"message":"x","error":{"code":"entitlement_required","feature":"custom_domain","upgrade_url":"https://supabase.com/dashboard/org/acme/billing"}}` - - t.Run("envelope sets suggestion with zero API calls", func(t *testing.T) { - t.Cleanup(func() { CmdSuggestion = "" }) - feature, slug, gated := SuggestUpgradeOnError(context.Background(), "ref", "", http.StatusBadRequest, []byte(envelope)) - assert.True(t, gated) - assert.Equal(t, "custom_domain", feature) - assert.Equal(t, "acme", slug) - assert.Contains(t, CmdSuggestion, "org/acme/billing") - assert.Contains(t, CmdSuggestion, "does not have access") - }) - - t.Run("envelope feature wins over call-site featureKey", func(t *testing.T) { - t.Cleanup(func() { CmdSuggestion = "" }) - body := `{"message":"x","error":{"code":"entitlement_required","feature":"branching_persistent","upgrade_url":"https://supabase.com/dashboard/org/acme/billing"}}` - feature, _, gated := SuggestUpgradeOnError(context.Background(), "ref", "branching_limit", http.StatusPaymentRequired, []byte(body)) - assert.True(t, gated) - assert.Equal(t, "branching_persistent", feature) - }) - - t.Run("envelope without upgrade_url falls back to entitlements", func(t *testing.T) { - ref := apitest.RandomProjectRef() - t.Cleanup(apitest.MockPlatformAPI(t)) - t.Cleanup(func() { CmdSuggestion = "" }) - mockEntitlementsCheck(ref, "branching_limit", false) - body := `{"message":"x","error":{"code":"entitlement_required","feature":"branching_limit"}}` - feature, slug, gated := SuggestUpgradeOnError(context.Background(), ref, "branching_limit", http.StatusPaymentRequired, []byte(body)) - assert.True(t, gated) - assert.Equal(t, "branching_limit", feature) - assert.Equal(t, "my-org", slug) - assert.Contains(t, CmdSuggestion, "/org/my-org/billing") - }) - - t.Run("malformed body falls back to entitlements", func(t *testing.T) { - ref := apitest.RandomProjectRef() - t.Cleanup(apitest.MockPlatformAPI(t)) - t.Cleanup(func() { CmdSuggestion = "" }) - mockEntitlementsCheck(ref, "branching_limit", false) - _, _, gated := SuggestUpgradeOnError(context.Background(), ref, "branching_limit", http.StatusPaymentRequired, []byte(`not json`)) - assert.True(t, gated) - }) - - t.Run("no envelope and empty featureKey is a no-op with zero API calls", func(t *testing.T) { - t.Cleanup(func() { CmdSuggestion = "" }) - _, _, gated := SuggestUpgradeOnError(context.Background(), "ref", "", http.StatusNotFound, []byte(`{"message":"not found"}`)) - assert.False(t, gated) - assert.Empty(t, CmdSuggestion) - }) - - t.Run("non-gate error code is ignored", func(t *testing.T) { - t.Cleanup(func() { CmdSuggestion = "" }) - body := `{"message":"x","error":{"code":"validation_failed","feature":"custom_domain","upgrade_url":"https://supabase.com/dashboard/org/acme/billing"}}` - _, _, gated := SuggestUpgradeOnError(context.Background(), "ref", "", http.StatusBadRequest, []byte(body)) - assert.False(t, gated) - assert.Empty(t, CmdSuggestion) - }) -} - -func TestOrgSlugFromBillingURL(t *testing.T) { - cases := map[string]string{ - "https://supabase.com/dashboard/org/acme/billing": "acme", - "https://supabase.com/dashboard/org/acme": "acme", - "https://example.com/nope": "", - } - for in, want := range cases { - assert.Equal(t, want, orgSlugFromBillingURL(in), in) - } -} diff --git a/apps/cli-go/internal/utils/profile.go b/apps/cli-go/internal/utils/profile.go deleted file mode 100644 index 4b24870271..0000000000 --- a/apps/cli-go/internal/utils/profile.go +++ /dev/null @@ -1,160 +0,0 @@ -package utils - -import ( - "context" - "fmt" - "path/filepath" - "sort" - "strings" - - "github.com/go-errors/errors" - "github.com/go-playground/validator/v10" - "github.com/spf13/afero" - "github.com/spf13/viper" - "github.com/supabase/cli/pkg/api" -) - -type Profile struct { - Name string `mapstructure:"name" validate:"required"` - APIURL string `mapstructure:"api_url" validate:"required,http_url"` - DashboardURL string `mapstructure:"dashboard_url" validate:"required,http_url"` - DocsURL string `mapstructure:"docs_url" validate:"omitempty,http_url"` - ProjectHost string `mapstructure:"project_host" validate:"required,hostname_rfc1123"` - PoolerHost string `mapstructure:"pooler_host" validate:"omitempty,hostname_rfc1123"` - AuthClientID string `mapstructure:"client_id" validate:"omitempty,uuid4"` - StudioImage string `mapstructure:"studio_image"` - ProjectRegions []api.V1CreateProjectBodyRegion `mapstructure:"regions"` -} - -var allProfiles = []Profile{{ - Name: "supabase", - APIURL: "https://api.supabase.com", - DashboardURL: "https://supabase.com/dashboard", - DocsURL: "https://supabase.com/docs", - ProjectHost: "supabase.co", - PoolerHost: "supabase.com", - ProjectRegions: []api.V1CreateProjectBodyRegion{ - api.V1CreateProjectBodyRegionApEast1, - api.V1CreateProjectBodyRegionApNortheast1, - api.V1CreateProjectBodyRegionApNortheast2, - api.V1CreateProjectBodyRegionApSouth1, - api.V1CreateProjectBodyRegionApSoutheast1, - api.V1CreateProjectBodyRegionApSoutheast2, - api.V1CreateProjectBodyRegionCaCentral1, - api.V1CreateProjectBodyRegionEuCentral1, - api.V1CreateProjectBodyRegionEuCentral2, - api.V1CreateProjectBodyRegionEuNorth1, - api.V1CreateProjectBodyRegionEuWest1, - api.V1CreateProjectBodyRegionEuWest2, - api.V1CreateProjectBodyRegionEuWest3, - api.V1CreateProjectBodyRegionSaEast1, - api.V1CreateProjectBodyRegionUsEast1, - api.V1CreateProjectBodyRegionUsEast2, - api.V1CreateProjectBodyRegionUsWest1, - api.V1CreateProjectBodyRegionUsWest2, - }, -}, { - Name: "supabase-staging", - APIURL: "https://api.supabase.green", - DashboardURL: "https://supabase.green/dashboard", - DocsURL: "https://supabase.com/docs", - ProjectHost: "supabase.red", - PoolerHost: "supabase.green", - ProjectRegions: []api.V1CreateProjectBodyRegion{ - api.V1CreateProjectBodyRegionApSoutheast1, - api.V1CreateProjectBodyRegionUsEast1, - api.V1CreateProjectBodyRegionEuCentral1, - }, -}, { - Name: "supabase-local", - APIURL: "http://localhost:8080", - DashboardURL: "http://localhost:8082", - DocsURL: "https://supabase.com/docs", - ProjectHost: "supabase.red", - ProjectRegions: []api.V1CreateProjectBodyRegion{ - api.V1CreateProjectBodyRegionApSoutheast1, - api.V1CreateProjectBodyRegionUsEast1, - api.V1CreateProjectBodyRegionEuCentral1, - }, -}, { - Name: "snap", - APIURL: "https://cloudapi.snap.com", - DashboardURL: "https://cloud.snap.com/dashboard", - DocsURL: "https://cloud.snap.com/docs", - ProjectHost: "snapcloud.dev", - PoolerHost: "snapcloud.co", - AuthClientID: "f7573b20-df47-48f1-b606-e8db4ec16252", - ProjectRegions: []api.V1CreateProjectBodyRegion{ - api.V1CreateProjectBodyRegionUsEast1, - }, -}} - -var CurrentProfile Profile - -func LoadProfile(ctx context.Context, fsys afero.Fs) error { - prof := getProfileName(fsys) - for _, p := range allProfiles { - if strings.EqualFold(p.Name, prof) { - CurrentProfile = p - return nil - } - } - // Instantiate to avoid leaking profile into global viper state - v := viper.New() - v.SetFs(fsys) - v.SetConfigFile(prof) - if err := v.ReadInConfig(); err != nil { - return errors.Errorf("failed to read profile: %w", err) - } - // Load profile into viper, rejecting keys not defined by config - if err := v.UnmarshalExact(&CurrentProfile); err != nil { - return errors.Errorf("failed to parse profile: %w", err) - } - validate := validator.New(validator.WithRequiredStructEnabled()) - if err := validate.StructCtx(ctx, &CurrentProfile); err != nil { - return errors.Errorf("invalid profile: %w", err) - } - return nil -} - -func getProfileName(fsys afero.Fs) string { - debuglogger := GetDebugLogger() - if prof := viper.GetString("PROFILE"); viper.IsSet("PROFILE") { - fmt.Fprintln(debuglogger, "Loading profile from flag:", prof) - return prof - } else if profilePath, err := getProfilePath(); err != nil { - fmt.Fprintln(debuglogger, err) - return prof - } else if content, err := afero.ReadFile(fsys, profilePath); err == nil { - fmt.Fprintln(debuglogger, "Loading profile from file:", profilePath) - return string(content) - } else { - fmt.Fprintln(debuglogger, err) - return prof - } -} - -func getProfilePath() (string, error) { - home, err := SupabaseHomeDir() - if err != nil { - return "", err - } - return filepath.Join(home, "profile"), nil -} - -func SaveProfileName(prof string, fsys afero.Fs) error { - profilePath, err := getProfilePath() - if err != nil { - return err - } - return WriteFile(profilePath, []byte(prof), fsys) -} - -func AwsRegions() []string { - result := make([]string, len(allProfiles[0].ProjectRegions)) - for i, region := range allProfiles[0].ProjectRegions { - result[i] = string(region) - } - sort.Strings(result) - return result -} diff --git a/apps/cli-go/internal/utils/profile_test.go b/apps/cli-go/internal/utils/profile_test.go deleted file mode 100644 index 4c5925e2d4..0000000000 --- a/apps/cli-go/internal/utils/profile_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package utils - -import ( - "context" - "embed" - "os" - "path/filepath" - "testing" - - "github.com/go-playground/validator/v10" - "github.com/spf13/afero" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" -) - -//go:embed testdata/*.json -var testdata embed.FS - -func TestLoadProfile(t *testing.T) { - validate := validator.New(validator.WithRequiredStructEnabled()) - for _, p := range allProfiles { - t.Run("loads profile "+p.Name, func(t *testing.T) { - viper.Set("PROFILE", p.Name) - t.Cleanup(viper.Reset) - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Run test - err := LoadProfile(context.Background(), fsys) - // Check error - assert.NoError(t, err) - assert.NoError(t, validate.Struct(&CurrentProfile)) - }) - } - - t.Run("loads from json", func(t *testing.T) { - viper.Set("PROFILE", "testdata/profile.json") - t.Cleanup(viper.Reset) - // Setup in-memory fs - fsys := afero.FromIOFS{FS: testdata} - // Run test - err := LoadProfile(context.Background(), fsys) - // Check error - assert.NoError(t, err) - }) - - t.Run("throws error on invalid profile", func(t *testing.T) { - viper.Set("PROFILE", "testdata/invalid.json") - t.Cleanup(viper.Reset) - // Setup in-memory fs - fsys := afero.FromIOFS{FS: testdata} - // Run test - err := LoadProfile(context.Background(), fsys) - // Check error - assert.ErrorContains(t, err, "Field validation for 'APIURL' failed on the 'http_url' tag") - assert.ErrorContains(t, err, "Field validation for 'ProjectHost' failed on the 'hostname_rfc1123' tag") - }) - - t.Run("throws error on malformed profile", func(t *testing.T) { - viper.Set("PROFILE", "testdata/malformed.json") - t.Cleanup(viper.Reset) - // Setup in-memory fs - fsys := afero.FromIOFS{FS: testdata} - // Run test - err := LoadProfile(context.Background(), fsys) - // Check error - assert.ErrorContains(t, err, "invalid keys: test_url") - }) - - t.Run("throws error on missing profile", func(t *testing.T) { - viper.Set("PROFILE", "testdata/missing.json") - t.Cleanup(viper.Reset) - // Setup in-memory fs - fsys := afero.FromIOFS{FS: testdata} - // Run test - err := LoadProfile(context.Background(), fsys) - // Check error - assert.ErrorIs(t, err, os.ErrNotExist) - }) -} - -func TestSaveProfileName(t *testing.T) { - t.Run("saves to SUPABASE_HOME when configured", func(t *testing.T) { - t.Setenv("HOME", "/home/test") - t.Setenv("SUPABASE_HOME", "/custom/supabase") - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Run test - err := SaveProfileName("supabase-staging", fsys) - // Check error - assert.NoError(t, err) - contents, err := afero.ReadFile(fsys, filepath.Join("/custom/supabase", "profile")) - assert.NoError(t, err) - assert.Equal(t, "supabase-staging", string(contents)) - exists, err := afero.Exists(fsys, filepath.Join("/home/test", ".supabase", "profile")) - assert.NoError(t, err) - assert.False(t, exists) - }) -} diff --git a/apps/cli-go/internal/utils/prompt.go b/apps/cli-go/internal/utils/prompt.go deleted file mode 100644 index ae2dedf2ae..0000000000 --- a/apps/cli-go/internal/utils/prompt.go +++ /dev/null @@ -1,144 +0,0 @@ -package utils - -import ( - "context" - "fmt" - "io" - "os" - "strings" - - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/go-errors/errors" -) - -var ( - titleStyle = lipgloss.NewStyle().MarginLeft(2) - itemStyle = lipgloss.NewStyle().PaddingLeft(4) - selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170")) - paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) - helpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1) -) - -// PromptItem is exposed as prompt input, empty summary + details will be excluded. -type PromptItem struct { - Summary string - Details string - Index int -} - -func (i PromptItem) Title() string { return i.Summary } -func (i PromptItem) Description() string { return i.Details } -func (i PromptItem) FilterValue() string { return i.Summary + " " + i.Details } - -// Item delegate is used to finetune the list item renderer. -type itemDelegate struct{} - -func (d itemDelegate) Height() int { return 1 } -func (d itemDelegate) Spacing() int { return 0 } -func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil } -func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { - i, ok := listItem.(PromptItem) - if !ok { - return - } - - str := fmt.Sprintf("%d. %s", index+1, i.Summary) - if i.Details != "" { - str += fmt.Sprintf(" [%s]", i.Details) - } - - fn := itemStyle.Render - if index == m.Index() { - fn = func(s ...string) string { - items := append([]string{"> "}, s...) - return selectedItemStyle.Render(items...) - } - } - - fmt.Fprint(w, fn(str)) -} - -// Model is used to store state of user choices. -type model struct { - cancel context.CancelFunc - list list.Model - choice PromptItem -} - -func (m model) Init() tea.Cmd { - return nil -} - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.list.SetWidth(msg.Width) - return m, nil - - case tea.KeyMsg: - switch msg.Type { - case tea.KeyCtrlC: - m.cancel() - return m, tea.Quit - - case tea.KeyEnter: - if choice, ok := m.list.SelectedItem().(PromptItem); ok { - m.choice = choice - } - return m, tea.Quit - } - } - - var cmd tea.Cmd - m.list, cmd = m.list.Update(msg) - return m, cmd -} - -func (m model) View() string { - if m.choice.Summary != "" { - return "" - } - return "\n" + m.list.View() -} - -// Prompt user to choose from a list of items, returns the chosen index. -func PromptChoice(ctx context.Context, title string, items []PromptItem, opts ...tea.ProgramOption) (PromptItem, error) { - // Create list items - var listItems []list.Item - for _, v := range items { - if strings.TrimSpace(v.FilterValue()) == "" { - continue - } - listItems = append(listItems, v) - } - // Create list model - height := min(len(listItems)*4, 14) - l := list.New(listItems, itemDelegate{}, 0, height) - l.Title = title - l.SetShowStatusBar(false) - l.Styles.Title = titleStyle - l.Styles.PaginationStyle = paginationStyle - l.Styles.HelpStyle = helpStyle - // Create our model - ctx, cancel := context.WithCancel(ctx) - initial := model{cancel: cancel, list: l} - // Interactive prompts should always be written to stderr - opts = append(opts, tea.WithOutput(os.Stderr)) - prog := tea.NewProgram(initial, opts...) - state, err := prog.Run() - if err != nil { - return initial.choice, errors.Errorf("failed to prompt choice: %w", err) - } - if ctx.Err() != nil { - return initial.choice, ctx.Err() - } - if m, ok := state.(model); ok { - if m.choice == initial.choice { - return initial.choice, errors.New("user aborted") - } - return m.choice, nil - } - return initial.choice, err -} diff --git a/apps/cli-go/internal/utils/release.go b/apps/cli-go/internal/utils/release.go deleted file mode 100644 index c68430c6aa..0000000000 --- a/apps/cli-go/internal/utils/release.go +++ /dev/null @@ -1,48 +0,0 @@ -package utils - -import ( - "context" - "net/http" - "os" - "sync" - - "github.com/go-errors/errors" - "github.com/google/go-github/v62/github" - "golang.org/x/oauth2" -) - -var ( - githubClient *github.Client - githubOnce sync.Once -) - -func GetGitHubClient(ctx context.Context) *github.Client { - githubOnce.Do(func() { - var client *http.Client - if token := os.Getenv("GITHUB_TOKEN"); len(token) > 0 { - ts := oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: token}, - ) - client = oauth2.NewClient(ctx, ts) - } - githubClient = github.NewClient(client) - }) - return githubClient -} - -const ( - CLI_OWNER = "supabase" - CLI_REPO = "cli" -) - -func GetLatestRelease(ctx context.Context) (string, error) { - client := GetGitHubClient(ctx) - release, _, err := client.Repositories.GetLatestRelease(ctx, CLI_OWNER, CLI_REPO) - if err != nil { - return "", errors.Errorf("Failed to fetch latest release: %w", err) - } - if release.TagName == nil { - return "", nil - } - return *release.TagName, nil -} diff --git a/apps/cli-go/internal/utils/release_test.go b/apps/cli-go/internal/utils/release_test.go deleted file mode 100644 index 25aa920e8f..0000000000 --- a/apps/cli-go/internal/utils/release_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package utils - -import ( - "context" - "net/http" - "testing" - - "github.com/go-errors/errors" - "github.com/google/go-github/v62/github" - "github.com/h2non/gock" - "github.com/stretchr/testify/assert" - "github.com/supabase/cli/internal/testing/apitest" - "github.com/supabase/cli/pkg/cast" -) - -func TestLatestRelease(t *testing.T) { - t.Run("fetches latest release", func(t *testing.T) { - // Setup api mock - defer gock.OffAll() - gock.New("https://api.github.com"). - Get("/repos/supabase/cli/releases/latest"). - Reply(http.StatusOK). - JSON(github.RepositoryRelease{TagName: cast.Ptr("v2")}) - // Run test - version, err := GetLatestRelease(context.Background()) - // Check error - assert.NoError(t, err) - assert.Equal(t, version, "v2") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("ignores missing version", func(t *testing.T) { - // Setup api mock - defer gock.OffAll() - gock.New("https://api.github.com"). - Get("/repos/supabase/cli/releases/latest"). - Reply(http.StatusOK). - JSON(github.RepositoryRelease{}) - // Run test - version, err := GetLatestRelease(context.Background()) - // Check error - assert.NoError(t, err) - assert.Empty(t, version) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on network error", func(t *testing.T) { - errNetwork := errors.New("network error") - // Setup api mock - defer gock.OffAll() - gock.New("https://api.github.com"). - Get("/repos/supabase/cli/releases/latest"). - ReplyError(errNetwork) - // Run test - version, err := GetLatestRelease(context.Background()) - // Check error - assert.ErrorIs(t, err, errNetwork) - assert.Empty(t, version) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} diff --git a/apps/cli-go/internal/utils/render.go b/apps/cli-go/internal/utils/render.go deleted file mode 100644 index ee4a4dc6c5..0000000000 --- a/apps/cli-go/internal/utils/render.go +++ /dev/null @@ -1,60 +0,0 @@ -package utils - -import ( - "fmt" - "time" -) - -const ( - layoutVersion = "20060102150405" - layoutHuman = "2006-01-02 15:04:05" -) - -func FormatTime(t time.Time) string { - return t.UTC().Format(layoutHuman) -} - -func FormatTimestamp(timestamp string) string { - return parse(time.RFC3339, timestamp) -} - -func FormatTimestampVersion(timestamp string) string { - return parse(layoutVersion, timestamp) -} - -func parse(layout, value string) string { - t, err := time.Parse(layout, value) - if err != nil { - fmt.Fprintln(GetDebugLogger(), err) - return value - } - return FormatTime(t) -} - -var regionMap = map[string]string{ - "ap-east-1": "East Asia (Hong Kong)", - "ap-northeast-1": "Northeast Asia (Tokyo)", - "ap-northeast-2": "Northeast Asia (Seoul)", - "ap-south-1": "South Asia (Mumbai)", - "ap-southeast-1": "Southeast Asia (Singapore)", - "ap-southeast-2": "Oceania (Sydney)", - "ca-central-1": "Canada (Central)", - "eu-central-1": "Central EU (Frankfurt)", - "eu-central-2": "Central Europe (Zurich)", - "eu-north-1": "North EU (Stockholm)", - "eu-west-1": "West EU (Ireland)", - "eu-west-2": "West Europe (London)", - "eu-west-3": "West EU (Paris)", - "sa-east-1": "South America (São Paulo)", - "us-east-1": "East US (North Virginia)", - "us-east-2": "East US (Ohio)", - "us-west-1": "West US (North California)", - "us-west-2": "West US (Oregon)", -} - -func FormatRegion(region string) string { - if readable, ok := regionMap[region]; ok { - return readable - } - return region -} diff --git a/apps/cli-go/internal/utils/retry.go b/apps/cli-go/internal/utils/retry.go deleted file mode 100644 index 6f0fb57e1d..0000000000 --- a/apps/cli-go/internal/utils/retry.go +++ /dev/null @@ -1,35 +0,0 @@ -package utils - -import ( - "context" - "fmt" - "os" - "time" - - "github.com/cenkalti/backoff/v4" -) - -const maxRetries = 8 - -func NewBackoffPolicy(ctx context.Context) backoff.BackOffContext { - b := backoff.NewExponentialBackOff(backoff.WithInitialInterval(3 * time.Second)) - return backoff.WithContext(backoff.WithMaxRetries(b, maxRetries), ctx) -} - -func NewErrorCallback(callbacks ...func(attempt uint) error) backoff.Notify { - failureCount := uint(0) - logger := GetDebugLogger() - return func(err error, d time.Duration) { - failureCount += 1 - if failureCount*3 > maxRetries { - logger = os.Stderr - } - for _, cb := range callbacks { - if err := cb(failureCount); err != nil { - fmt.Fprintln(logger, err) - } - } - fmt.Fprintln(logger, err) - fmt.Fprintf(logger, "Retry (%d/%d): ", failureCount, maxRetries) - } -} diff --git a/apps/cli-go/internal/utils/slice.go b/apps/cli-go/internal/utils/slice.go deleted file mode 100644 index 9babf6c59c..0000000000 --- a/apps/cli-go/internal/utils/slice.go +++ /dev/null @@ -1,12 +0,0 @@ -package utils - -func RemoveDuplicates[T comparable](slice []T) (result []T) { - set := make(map[T]struct{}) - for _, item := range slice { - if _, exists := set[item]; !exists { - set[item] = struct{}{} - result = append(result, item) - } - } - return result -} diff --git a/apps/cli-go/internal/utils/slice_test.go b/apps/cli-go/internal/utils/slice_test.go deleted file mode 100644 index 3c2a3e9575..0000000000 --- a/apps/cli-go/internal/utils/slice_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package utils - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRemoveDuplicates(t *testing.T) { - t.Run("string slice", func(t *testing.T) { - input := []string{"a", "b", "a", "c", "b", "d"} - expected := []string{"a", "b", "c", "d"} - assert.Equal(t, expected, RemoveDuplicates(input)) - }) - - t.Run("int slice", func(t *testing.T) { - input := []int{1, 2, 2, 3, 1, 4, 3, 5} - expected := []int{1, 2, 3, 4, 5} - assert.Equal(t, expected, RemoveDuplicates(input)) - }) - - t.Run("empty slice", func(t *testing.T) { - assert.Empty(t, RemoveDuplicates([]string{})) - }) - - t.Run("no duplicates", func(t *testing.T) { - input := []int{1, 2, 3, 4, 5} - assert.Equal(t, input, RemoveDuplicates(input)) - }) -} diff --git a/apps/cli-go/internal/utils/supabase_home.go b/apps/cli-go/internal/utils/supabase_home.go deleted file mode 100644 index 02dba18b10..0000000000 --- a/apps/cli-go/internal/utils/supabase_home.go +++ /dev/null @@ -1,24 +0,0 @@ -package utils - -import ( - "os" - "path/filepath" - "strings" - - "github.com/go-errors/errors" -) - -// SupabaseHomeDir returns the global Supabase CLI state root. It is overridden -// by the SUPABASE_HOME environment variable when set to a non-empty value (an -// absolute path is expected; the value is used verbatim), otherwise it defaults -// to ~/.supabase. -func SupabaseHomeDir() (string, error) { - if home := strings.TrimSpace(os.Getenv("SUPABASE_HOME")); home != "" { - return home, nil - } - home, err := os.UserHomeDir() - if err != nil { - return "", errors.Errorf("failed to get $HOME directory: %w", err) - } - return filepath.Join(home, ".supabase"), nil -} diff --git a/apps/cli-go/internal/utils/tea.go b/apps/cli-go/internal/utils/tea.go deleted file mode 100644 index b7965eebfc..0000000000 --- a/apps/cli-go/internal/utils/tea.go +++ /dev/null @@ -1,201 +0,0 @@ -package utils - -import ( - "bytes" - "context" - "fmt" - "os" - "strings" - - "github.com/charmbracelet/bubbles/progress" - "github.com/charmbracelet/bubbles/spinner" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/muesli/reflow/wrap" - "golang.org/x/term" -) - -func NewProgram(model tea.Model, opts ...tea.ProgramOption) Program { - var p Program - if term.IsTerminal(int(os.Stdin.Fd())) { - p = tea.NewProgram(model, opts...) - } else { - p = newFakeProgram(model) - } - return p -} - -// An interface describing the parts of BubbleTea's Program that we actually use. -type Program interface { - Start() error - Send(msg tea.Msg) - Quit() -} - -func newFakeProgram(model tea.Model) *fakeProgram { - p := &fakeProgram{ - model: model, - } - return p -} - -// A dumb text implementation of BubbleTea's Program that allows -// for output to be piped to another program. -type fakeProgram struct { - model tea.Model -} - -func (p *fakeProgram) Start() error { - initCmd := p.model.Init() - message := initCmd() - if message != nil { - p.model.Update(message) - } - return nil -} - -func (p *fakeProgram) Send(msg tea.Msg) { - switch msg := msg.(type) { - case StatusMsg: - fmt.Println(msg) - case PsqlMsg: - if msg != nil { - fmt.Println(*msg) - } - } - - _, cmd := p.model.Update(msg) - if cmd != nil { - cmd() - } -} - -func (p *fakeProgram) Quit() { - p.Send(tea.Quit()) -} - -type ( - StatusMsg string - ProgressMsg *float64 - PsqlMsg *string -) - -type StatusWriter struct { - Program -} - -func (t StatusWriter) Write(p []byte) (int, error) { - trimmed := bytes.TrimRight(p, "\n") - t.Send(StatusMsg(trimmed)) - return len(p), nil -} - -func RunProgram(ctx context.Context, f func(p Program, ctx context.Context) error) error { - ctx, cancel := context.WithCancel(ctx) - p := NewProgram(logModel{ - cancel: cancel, - spinner: spinner.New( - spinner.WithSpinner(spinner.Dot), - spinner.WithStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("205"))), - ), - }) - - errCh := make(chan error, 1) - go func() { - errCh <- f(p, ctx) - p.Quit() - }() - - if err := p.Start(); err != nil { - return err - } - return <-errCh -} - -type logModel struct { - cancel context.CancelFunc - - spinner spinner.Model - status string - progress *progress.Model - psqlOutputs []string - - width int -} - -func (m logModel) Init() tea.Cmd { - return m.spinner.Tick -} - -func (m logModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.Type { - case tea.KeyCtrlC: - if m.cancel != nil { - m.cancel() - } - return m, tea.Quit - default: - return m, nil - } - case tea.WindowSizeMsg: - m.width = msg.Width - return m, nil - case spinner.TickMsg: - spinnerModel, cmd := m.spinner.Update(msg) - m.spinner = spinnerModel - return m, cmd - case progress.FrameMsg: - if m.progress == nil { - return m, nil - } - - tmp, cmd := m.progress.Update(msg) - progressModel := tmp.(progress.Model) - m.progress = &progressModel - return m, cmd - case StatusMsg: - m.status = string(msg) - return m, nil - case ProgressMsg: - if msg == nil { - m.progress = nil - return m, nil - } - - if m.progress == nil { - progressModel := progress.New(progress.WithGradient("#1c1c1c", "#34b27b")) - m.progress = &progressModel - } - - return m, m.progress.SetPercent(*msg) - case PsqlMsg: - if msg == nil { - m.psqlOutputs = []string{} - return m, nil - } - - m.psqlOutputs = append(m.psqlOutputs, *msg) - if len(m.psqlOutputs) > 5 { - m.psqlOutputs = m.psqlOutputs[1:] - } - return m, nil - default: - return m, nil - } -} - -func (m logModel) View() string { - var progress string - if m.progress != nil { - progress = "\n\n" + m.progress.View() - } - - var psqlOutputs string - if len(m.psqlOutputs) > 0 { - psqlOutputs = "\n\n" + strings.Join(m.psqlOutputs, "\n") - } - - return wrap.String(m.spinner.View()+m.status+progress+psqlOutputs, m.width) -} diff --git a/apps/cli-go/internal/utils/templates/globals.sql b/apps/cli-go/internal/utils/templates/globals.sql deleted file mode 100644 index 0b8fbaef01..0000000000 --- a/apps/cli-go/internal/utils/templates/globals.sql +++ /dev/null @@ -1,146 +0,0 @@ --- --- PostgreSQL database cluster dump --- - -SET default_transaction_read_only = off; - -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; - --- --- Roles --- - -CREATE ROLE anon; -ALTER ROLE anon WITH NOSUPERUSER NOINHERIT NOCREATEROLE NOCREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS; -CREATE ROLE authenticated; -ALTER ROLE authenticated WITH NOSUPERUSER NOINHERIT NOCREATEROLE NOCREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS; -CREATE ROLE authenticator; -ALTER ROLE authenticator WITH NOSUPERUSER NOINHERIT NOCREATEROLE NOCREATEDB LOGIN NOREPLICATION NOBYPASSRLS PASSWORD 'postgres'; -CREATE ROLE dashboard_user; -ALTER ROLE dashboard_user WITH NOSUPERUSER INHERIT CREATEROLE CREATEDB NOLOGIN REPLICATION NOBYPASSRLS; -CREATE ROLE pgbouncer; -ALTER ROLE pgbouncer WITH NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB LOGIN NOREPLICATION NOBYPASSRLS PASSWORD 'postgres'; --- CREATE ROLE pgsodium_keyholder; --- ALTER ROLE pgsodium_keyholder WITH NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS; --- CREATE ROLE pgsodium_keyiduser; --- ALTER ROLE pgsodium_keyiduser WITH NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS; --- CREATE ROLE pgsodium_keymaker; --- ALTER ROLE pgsodium_keymaker WITH NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS; --- CREATE ROLE postgres; --- ALTER ROLE postgres WITH NOSUPERUSER INHERIT CREATEROLE CREATEDB LOGIN REPLICATION BYPASSRLS; -CREATE ROLE service_role; -ALTER ROLE service_role WITH NOSUPERUSER NOINHERIT NOCREATEROLE NOCREATEDB NOLOGIN NOREPLICATION BYPASSRLS; -CREATE ROLE supabase_admin; -ALTER ROLE supabase_admin WITH SUPERUSER INHERIT CREATEROLE CREATEDB LOGIN REPLICATION BYPASSRLS PASSWORD 'postgres'; -CREATE ROLE supabase_auth_admin; -ALTER ROLE supabase_auth_admin WITH NOSUPERUSER NOINHERIT CREATEROLE NOCREATEDB LOGIN NOREPLICATION NOBYPASSRLS PASSWORD 'postgres'; -CREATE ROLE supabase_functions_admin; -ALTER ROLE supabase_functions_admin WITH NOSUPERUSER NOINHERIT CREATEROLE NOCREATEDB LOGIN NOREPLICATION NOBYPASSRLS PASSWORD 'postgres'; -CREATE ROLE supabase_read_only_user; -ALTER ROLE supabase_read_only_user WITH NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB LOGIN NOREPLICATION BYPASSRLS PASSWORD 'postgres'; -CREATE ROLE supabase_replication_admin; -ALTER ROLE supabase_replication_admin WITH NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB LOGIN REPLICATION NOBYPASSRLS PASSWORD 'postgres'; -CREATE ROLE supabase_storage_admin; -ALTER ROLE supabase_storage_admin WITH NOSUPERUSER NOINHERIT CREATEROLE NOCREATEDB LOGIN NOREPLICATION NOBYPASSRLS PASSWORD 'postgres'; - --- --- User Configurations --- - --- --- User Config "anon" --- - -ALTER ROLE anon SET statement_timeout TO '3s'; - --- --- User Config "authenticated" --- - -ALTER ROLE authenticated SET statement_timeout TO '8s'; - --- --- User Config "authenticator" --- - -ALTER ROLE authenticator SET session_preload_libraries TO 'safeupdate'; -ALTER ROLE authenticator SET statement_timeout TO '8s'; - --- --- User Config "postgres" --- - -ALTER ROLE postgres SET search_path TO E'\\$user', 'public', 'extensions'; - --- --- User Config "supabase_admin" --- - -ALTER ROLE supabase_admin SET search_path TO E'\\$user', 'public', 'auth', 'extensions'; - --- --- User Config "supabase_auth_admin" --- - -ALTER ROLE supabase_auth_admin SET search_path TO 'auth'; -ALTER ROLE supabase_auth_admin SET idle_in_transaction_session_timeout TO '60000'; - --- --- User Config "supabase_functions_admin" --- - -ALTER ROLE supabase_functions_admin SET search_path TO 'supabase_functions'; - --- --- User Config "supabase_storage_admin" --- - -ALTER ROLE supabase_storage_admin SET search_path TO 'storage'; - - --- --- Role memberships --- - -GRANT anon TO authenticator GRANTED BY postgres; -GRANT anon TO postgres GRANTED BY supabase_admin; -GRANT anon TO supabase_storage_admin GRANTED BY supabase_admin; -GRANT authenticated TO authenticator GRANTED BY postgres; -GRANT authenticated TO postgres GRANTED BY supabase_admin; -GRANT authenticated TO supabase_storage_admin GRANTED BY supabase_admin; -GRANT pg_monitor TO postgres GRANTED BY supabase_admin; --- GRANT pg_read_all_data TO supabase_read_only_user GRANTED BY postgres; --- GRANT pgsodium_keyholder TO pgsodium_keymaker GRANTED BY postgres; --- GRANT pgsodium_keyholder TO postgres WITH ADMIN OPTION GRANTED BY postgres; --- GRANT pgsodium_keyiduser TO pgsodium_keyholder GRANTED BY postgres; --- GRANT pgsodium_keyiduser TO pgsodium_keymaker GRANTED BY postgres; --- GRANT pgsodium_keyiduser TO postgres WITH ADMIN OPTION GRANTED BY postgres; --- GRANT pgsodium_keymaker TO postgres WITH ADMIN OPTION GRANTED BY postgres; -GRANT service_role TO authenticator GRANTED BY postgres; -GRANT service_role TO postgres GRANTED BY supabase_admin; -GRANT service_role TO supabase_storage_admin GRANTED BY supabase_admin; -GRANT supabase_auth_admin TO postgres GRANTED BY supabase_admin; -GRANT supabase_functions_admin TO postgres GRANTED BY supabase_admin; -GRANT supabase_storage_admin TO postgres GRANTED BY supabase_admin; - - - - --- --- PostgreSQL database cluster dump complete --- - -DO $$ -BEGIN - -- role pg_read_all_data is not available on pg13 - IF EXISTS ( - SELECT FROM pg_catalog.pg_roles - WHERE rolname = 'pg_read_all_data' - ) THEN - GRANT pg_read_all_data TO supabase_read_only_user GRANTED BY postgres; - END IF; -END -$$; - -RESET ALL; diff --git a/apps/cli-go/internal/utils/templates/initial_schemas/13.sql b/apps/cli-go/internal/utils/templates/initial_schemas/13.sql deleted file mode 100644 index 135ce9a516..0000000000 --- a/apps/cli-go/internal/utils/templates/initial_schemas/13.sql +++ /dev/null @@ -1,3094 +0,0 @@ --- --- PostgreSQL database dump --- - --- Dumped from database version 13.3 (Debian 13.3-1.pgdg100+1) --- Dumped by pg_dump version 15.3 - -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - --- --- Name: _analytics; Type: SCHEMA; Schema: -; Owner: postgres --- - -CREATE SCHEMA IF NOT EXISTS _analytics; - - -ALTER SCHEMA _analytics OWNER TO postgres; - --- --- Name: _realtime; Type: SCHEMA; Schema: -; Owner: postgres --- - -CREATE SCHEMA IF NOT EXISTS _realtime; - - -ALTER SCHEMA _realtime OWNER TO postgres; - --- --- Name: auth; Type: SCHEMA; Schema: -; Owner: supabase_admin --- - -CREATE SCHEMA IF NOT EXISTS auth; - - -ALTER SCHEMA auth OWNER TO supabase_admin; - --- --- Name: extensions; Type: SCHEMA; Schema: -; Owner: postgres --- - -CREATE SCHEMA IF NOT EXISTS extensions; - - -ALTER SCHEMA extensions OWNER TO postgres; - --- --- Name: graphql_public; Type: SCHEMA; Schema: -; Owner: supabase_admin --- - -CREATE SCHEMA IF NOT EXISTS graphql_public; - - -ALTER SCHEMA graphql_public OWNER TO supabase_admin; - --- --- Name: public; Type: SCHEMA; Schema: -; Owner: postgres --- - --- *not* creating schema, since initdb creates it - - -ALTER SCHEMA public OWNER TO postgres; - --- --- Name: realtime; Type: SCHEMA; Schema: -; Owner: supabase_admin --- - -CREATE SCHEMA IF NOT EXISTS realtime; - - -ALTER SCHEMA realtime OWNER TO supabase_admin; - --- --- Name: storage; Type: SCHEMA; Schema: -; Owner: supabase_admin --- - -CREATE SCHEMA IF NOT EXISTS storage; - - -ALTER SCHEMA storage OWNER TO supabase_admin; - --- --- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: - --- - -CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA extensions; - - --- --- Name: EXTENSION pgcrypto; Type: COMMENT; Schema: -; Owner: --- - -COMMENT ON EXTENSION pgcrypto IS 'cryptographic functions'; - - --- --- Name: pgjwt; Type: EXTENSION; Schema: -; Owner: - --- - -CREATE EXTENSION IF NOT EXISTS pgjwt WITH SCHEMA extensions; - - --- --- Name: EXTENSION pgjwt; Type: COMMENT; Schema: -; Owner: --- - -COMMENT ON EXTENSION pgjwt IS 'JSON Web Token API for Postgresql'; - - --- --- Name: uuid-ossp; Type: EXTENSION; Schema: -; Owner: - --- - -CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA extensions; - - --- --- Name: EXTENSION "uuid-ossp"; Type: COMMENT; Schema: -; Owner: --- - -COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UUIDs)'; - - --- --- Name: aal_level; Type: TYPE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TYPE auth.aal_level AS ENUM ( - 'aal1', - 'aal2', - 'aal3' -); - - -ALTER TYPE auth.aal_level OWNER TO supabase_auth_admin; - --- --- Name: code_challenge_method; Type: TYPE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TYPE auth.code_challenge_method AS ENUM ( - 's256', - 'plain' -); - - -ALTER TYPE auth.code_challenge_method OWNER TO supabase_auth_admin; - --- --- Name: factor_status; Type: TYPE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TYPE auth.factor_status AS ENUM ( - 'unverified', - 'verified' -); - - -ALTER TYPE auth.factor_status OWNER TO supabase_auth_admin; - --- --- Name: factor_type; Type: TYPE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TYPE auth.factor_type AS ENUM ( - 'totp', - 'webauthn' -); - - -ALTER TYPE auth.factor_type OWNER TO supabase_auth_admin; - --- --- Name: email(); Type: FUNCTION; Schema: auth; Owner: supabase_auth_admin --- - -CREATE OR REPLACE FUNCTION auth.email() RETURNS text - LANGUAGE sql STABLE - AS $$ - select - coalesce( - nullif(current_setting('request.jwt.claim.email', true), ''), - (nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'email') - )::text -$$; - - -ALTER FUNCTION auth.email() OWNER TO supabase_auth_admin; - --- --- Name: FUNCTION email(); Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON FUNCTION auth.email() IS 'Deprecated. Use auth.jwt() -> ''email'' instead.'; - - --- --- Name: jwt(); Type: FUNCTION; Schema: auth; Owner: supabase_auth_admin --- - -CREATE OR REPLACE FUNCTION auth.jwt() RETURNS jsonb - LANGUAGE sql STABLE - AS $$ - select - coalesce( - nullif(current_setting('request.jwt.claim', true), ''), - nullif(current_setting('request.jwt.claims', true), '') - )::jsonb -$$; - - -ALTER FUNCTION auth.jwt() OWNER TO supabase_auth_admin; - --- --- Name: role(); Type: FUNCTION; Schema: auth; Owner: supabase_auth_admin --- - -CREATE OR REPLACE FUNCTION auth.role() RETURNS text - LANGUAGE sql STABLE - AS $$ - select - coalesce( - nullif(current_setting('request.jwt.claim.role', true), ''), - (nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'role') - )::text -$$; - - -ALTER FUNCTION auth.role() OWNER TO supabase_auth_admin; - --- --- Name: FUNCTION role(); Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON FUNCTION auth.role() IS 'Deprecated. Use auth.jwt() -> ''role'' instead.'; - - --- --- Name: uid(); Type: FUNCTION; Schema: auth; Owner: supabase_auth_admin --- - -CREATE OR REPLACE FUNCTION auth.uid() RETURNS uuid - LANGUAGE sql STABLE - AS $$ - select - coalesce( - nullif(current_setting('request.jwt.claim.sub', true), ''), - (nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub') - )::uuid -$$; - - -ALTER FUNCTION auth.uid() OWNER TO supabase_auth_admin; - --- --- Name: FUNCTION uid(); Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON FUNCTION auth.uid() IS 'Deprecated. Use auth.jwt() -> ''sub'' instead.'; - - --- --- Name: grant_pg_cron_access(); Type: FUNCTION; Schema: extensions; Owner: postgres --- - -CREATE OR REPLACE FUNCTION extensions.grant_pg_cron_access() RETURNS event_trigger - LANGUAGE plpgsql - AS $$ -DECLARE - schema_is_cron bool; -BEGIN - schema_is_cron = ( - SELECT n.nspname = 'cron' - FROM pg_event_trigger_ddl_commands() AS ev - LEFT JOIN pg_catalog.pg_namespace AS n - ON ev.objid = n.oid - ); - - IF schema_is_cron - THEN - grant usage on schema cron to postgres with grant option; - - alter default privileges in schema cron grant all on tables to postgres with grant option; - alter default privileges in schema cron grant all on functions to postgres with grant option; - alter default privileges in schema cron grant all on sequences to postgres with grant option; - - alter default privileges for user supabase_admin in schema cron grant all - on sequences to postgres with grant option; - alter default privileges for user supabase_admin in schema cron grant all - on tables to postgres with grant option; - alter default privileges for user supabase_admin in schema cron grant all - on functions to postgres with grant option; - - grant all privileges on all tables in schema cron to postgres with grant option; - - END IF; - -END; -$$; - - -ALTER FUNCTION extensions.grant_pg_cron_access() OWNER TO postgres; - --- --- Name: FUNCTION grant_pg_cron_access(); Type: COMMENT; Schema: extensions; Owner: postgres --- - -COMMENT ON FUNCTION extensions.grant_pg_cron_access() IS 'Grants access to pg_cron'; - - --- --- Name: grant_pg_graphql_access(); Type: FUNCTION; Schema: extensions; Owner: supabase_admin --- - -CREATE OR REPLACE FUNCTION extensions.grant_pg_graphql_access() RETURNS event_trigger - LANGUAGE plpgsql - AS $_$ -DECLARE - func_is_graphql_resolve bool; -BEGIN - func_is_graphql_resolve = ( - SELECT n.proname = 'resolve' - FROM pg_event_trigger_ddl_commands() AS ev - LEFT JOIN pg_catalog.pg_proc AS n - ON ev.objid = n.oid - ); - - IF func_is_graphql_resolve - THEN - - -- Update public wrapper to pass all arguments through to the pg_graphql resolve func - create or replace function graphql_public.graphql( - "operationName" text default null, - query text default null, - variables jsonb default null, - extensions jsonb default null - ) - returns jsonb - language sql - as $$ - select graphql.resolve( - query := query, - variables := coalesce(variables, '{}'), - "operationName" := "operationName", - extensions := extensions - ); - $$; - - -- This hook executes when `graphql.resolve` is created. That is not necessarily the last - -- function in the extension so we need to grant permissions on existing entities AND - -- update default permissions to any others that are created after `graphql.resolve` - grant usage on schema graphql to postgres, anon, authenticated, service_role; - grant select on all tables in schema graphql to postgres, anon, authenticated, service_role; - grant execute on all functions in schema graphql to postgres, anon, authenticated, service_role; - grant all on all sequences in schema graphql to postgres, anon, authenticated, service_role; - alter default privileges in schema graphql grant all on tables to postgres, anon, authenticated, service_role; - alter default privileges in schema graphql grant all on functions to postgres, anon, authenticated, service_role; - alter default privileges in schema graphql grant all on sequences to postgres, anon, authenticated, service_role; - END IF; - -END; -$_$; - - -ALTER FUNCTION extensions.grant_pg_graphql_access() OWNER TO supabase_admin; - --- --- Name: FUNCTION grant_pg_graphql_access(); Type: COMMENT; Schema: extensions; Owner: supabase_admin --- - -COMMENT ON FUNCTION extensions.grant_pg_graphql_access() IS 'Grants access to pg_graphql'; - - --- --- Name: grant_pg_net_access(); Type: FUNCTION; Schema: extensions; Owner: postgres --- - -CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access() RETURNS event_trigger - LANGUAGE plpgsql - AS $$ -BEGIN - IF EXISTS ( - SELECT 1 - FROM pg_event_trigger_ddl_commands() AS ev - JOIN pg_extension AS ext - ON ev.objid = ext.oid - WHERE ext.extname = 'pg_net' - ) - THEN - IF NOT EXISTS ( - SELECT 1 - FROM pg_roles - WHERE rolname = 'supabase_functions_admin' - ) - THEN - CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION; - END IF; - - GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role; - - ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; - ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; - ALTER function net.http_collect_response(request_id bigint, async boolean) SECURITY DEFINER; - - ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; - ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; - ALTER function net.http_collect_response(request_id bigint, async boolean) SET search_path = net; - - REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; - REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; - REVOKE ALL ON FUNCTION net.http_collect_response(request_id bigint, async boolean) FROM PUBLIC; - - GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; - GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; - GRANT EXECUTE ON FUNCTION net.http_collect_response(request_id bigint, async boolean) TO supabase_functions_admin, postgres, anon, authenticated, service_role; - END IF; -END; -$$; - - -ALTER FUNCTION extensions.grant_pg_net_access() OWNER TO postgres; - --- --- Name: FUNCTION grant_pg_net_access(); Type: COMMENT; Schema: extensions; Owner: postgres --- - -COMMENT ON FUNCTION extensions.grant_pg_net_access() IS 'Grants access to pg_net'; - - --- --- Name: pgrst_ddl_watch(); Type: FUNCTION; Schema: extensions; Owner: supabase_admin --- - -CREATE OR REPLACE FUNCTION extensions.pgrst_ddl_watch() RETURNS event_trigger - LANGUAGE plpgsql - AS $$ -DECLARE - cmd record; -BEGIN - FOR cmd IN SELECT * FROM pg_event_trigger_ddl_commands() - LOOP - IF cmd.command_tag IN ( - 'CREATE SCHEMA', 'ALTER SCHEMA' - , 'CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO', 'ALTER TABLE' - , 'CREATE FOREIGN TABLE', 'ALTER FOREIGN TABLE' - , 'CREATE VIEW', 'ALTER VIEW' - , 'CREATE MATERIALIZED VIEW', 'ALTER MATERIALIZED VIEW' - , 'CREATE FUNCTION', 'ALTER FUNCTION' - , 'CREATE TRIGGER' - , 'CREATE TYPE', 'ALTER TYPE' - , 'CREATE RULE' - , 'COMMENT' - ) - -- don't notify in case of CREATE TEMP table or other objects created on pg_temp - AND cmd.schema_name is distinct from 'pg_temp' - THEN - NOTIFY pgrst, 'reload schema'; - END IF; - END LOOP; -END; $$; - - -ALTER FUNCTION extensions.pgrst_ddl_watch() OWNER TO supabase_admin; - --- --- Name: pgrst_drop_watch(); Type: FUNCTION; Schema: extensions; Owner: supabase_admin --- - -CREATE OR REPLACE FUNCTION extensions.pgrst_drop_watch() RETURNS event_trigger - LANGUAGE plpgsql - AS $$ -DECLARE - obj record; -BEGIN - FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects() - LOOP - IF obj.object_type IN ( - 'schema' - , 'table' - , 'foreign table' - , 'view' - , 'materialized view' - , 'function' - , 'trigger' - , 'type' - , 'rule' - ) - AND obj.is_temporary IS false -- no pg_temp objects - THEN - NOTIFY pgrst, 'reload schema'; - END IF; - END LOOP; -END; $$; - - -ALTER FUNCTION extensions.pgrst_drop_watch() OWNER TO supabase_admin; - --- --- Name: set_graphql_placeholder(); Type: FUNCTION; Schema: extensions; Owner: supabase_admin --- - -CREATE OR REPLACE FUNCTION extensions.set_graphql_placeholder() RETURNS event_trigger - LANGUAGE plpgsql - AS $_$ - DECLARE - graphql_is_dropped bool; - BEGIN - graphql_is_dropped = ( - SELECT ev.schema_name = 'graphql_public' - FROM pg_event_trigger_dropped_objects() AS ev - WHERE ev.schema_name = 'graphql_public' - ); - - IF graphql_is_dropped - THEN - create or replace function graphql_public.graphql( - "operationName" text default null, - query text default null, - variables jsonb default null, - extensions jsonb default null - ) - returns jsonb - language plpgsql - as $$ - DECLARE - server_version float; - BEGIN - server_version = (SELECT (SPLIT_PART((select version()), ' ', 2))::float); - - IF server_version >= 14 THEN - RETURN jsonb_build_object( - 'errors', jsonb_build_array( - jsonb_build_object( - 'message', 'pg_graphql extension is not enabled.' - ) - ) - ); - ELSE - RETURN jsonb_build_object( - 'errors', jsonb_build_array( - jsonb_build_object( - 'message', 'pg_graphql is only available on projects running Postgres 14 onwards.' - ) - ) - ); - END IF; - END; - $$; - END IF; - - END; -$_$; - - -ALTER FUNCTION extensions.set_graphql_placeholder() OWNER TO supabase_admin; - --- --- Name: FUNCTION set_graphql_placeholder(); Type: COMMENT; Schema: extensions; Owner: supabase_admin --- - -COMMENT ON FUNCTION extensions.set_graphql_placeholder() IS 'Reintroduces placeholder function for graphql_public.graphql'; - - --- --- Name: graphql(text, text, jsonb, jsonb); Type: FUNCTION; Schema: graphql_public; Owner: supabase_admin --- - -CREATE OR REPLACE FUNCTION graphql_public.graphql("operationName" text DEFAULT NULL::text, query text DEFAULT NULL::text, variables jsonb DEFAULT NULL::jsonb, extensions jsonb DEFAULT NULL::jsonb) RETURNS jsonb - LANGUAGE plpgsql - AS $$ - DECLARE - server_version float; - BEGIN - server_version = (SELECT (SPLIT_PART((select version()), ' ', 2))::float); - - IF server_version >= 14 THEN - RETURN jsonb_build_object( - 'errors', jsonb_build_array( - jsonb_build_object( - 'message', 'pg_graphql extension is not enabled.' - ) - ) - ); - ELSE - RETURN jsonb_build_object( - 'errors', jsonb_build_array( - jsonb_build_object( - 'message', 'pg_graphql is only available on projects running Postgres 14 onwards.' - ) - ) - ); - END IF; - END; -$$; - - -ALTER FUNCTION graphql_public.graphql("operationName" text, query text, variables jsonb, extensions jsonb) OWNER TO supabase_admin; - --- --- Name: can_insert_object(text, text, uuid, jsonb); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin --- - -CREATE OR REPLACE FUNCTION storage.can_insert_object(bucketid text, name text, owner uuid, metadata jsonb) RETURNS void - LANGUAGE plpgsql - AS $$ -BEGIN - INSERT INTO "storage"."objects" ("bucket_id", "name", "owner", "metadata") VALUES (bucketid, name, owner, metadata); - -- hack to rollback the successful insert - RAISE sqlstate 'PT200' using - message = 'ROLLBACK', - detail = 'rollback successful insert'; -END -$$; - - -ALTER FUNCTION storage.can_insert_object(bucketid text, name text, owner uuid, metadata jsonb) OWNER TO supabase_storage_admin; - --- --- Name: extension(text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin --- - -CREATE OR REPLACE FUNCTION storage.extension(name text) RETURNS text - LANGUAGE plpgsql - AS $$ -DECLARE -_parts text[]; -_filename text; -BEGIN - select string_to_array(name, '/') into _parts; - select _parts[array_length(_parts,1)] into _filename; - -- @todo return the last part instead of 2 - return split_part(_filename, '.', 2); -END -$$; - - -ALTER FUNCTION storage.extension(name text) OWNER TO supabase_storage_admin; - --- --- Name: filename(text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin --- - -CREATE OR REPLACE FUNCTION storage.filename(name text) RETURNS text - LANGUAGE plpgsql - AS $$ -DECLARE -_parts text[]; -BEGIN - select string_to_array(name, '/') into _parts; - return _parts[array_length(_parts,1)]; -END -$$; - - -ALTER FUNCTION storage.filename(name text) OWNER TO supabase_storage_admin; - --- --- Name: foldername(text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin --- - -CREATE OR REPLACE FUNCTION storage.foldername(name text) RETURNS text[] - LANGUAGE plpgsql - AS $$ -DECLARE -_parts text[]; -BEGIN - select string_to_array(name, '/') into _parts; - return _parts[1:array_length(_parts,1)-1]; -END -$$; - - -ALTER FUNCTION storage.foldername(name text) OWNER TO supabase_storage_admin; - --- --- Name: get_size_by_bucket(); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin --- - -CREATE OR REPLACE FUNCTION storage.get_size_by_bucket() RETURNS TABLE(size bigint, bucket_id text) - LANGUAGE plpgsql - AS $$ -BEGIN - return query - select sum((metadata->>'size')::int) as size, obj.bucket_id - from "storage".objects as obj - group by obj.bucket_id; -END -$$; - - -ALTER FUNCTION storage.get_size_by_bucket() OWNER TO supabase_storage_admin; - --- --- Name: search(text, text, integer, integer, integer, text, text, text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin --- - -CREATE OR REPLACE FUNCTION storage.search(prefix text, bucketname text, limits integer DEFAULT 100, levels integer DEFAULT 1, offsets integer DEFAULT 0, search text DEFAULT ''::text, sortcolumn text DEFAULT 'name'::text, sortorder text DEFAULT 'asc'::text) RETURNS TABLE(name text, id uuid, updated_at timestamp with time zone, created_at timestamp with time zone, last_accessed_at timestamp with time zone, metadata jsonb) - LANGUAGE plpgsql STABLE - AS $_$ -declare - v_order_by text; - v_sort_order text; -begin - case - when sortcolumn = 'name' then - v_order_by = 'name'; - when sortcolumn = 'updated_at' then - v_order_by = 'updated_at'; - when sortcolumn = 'created_at' then - v_order_by = 'created_at'; - when sortcolumn = 'last_accessed_at' then - v_order_by = 'last_accessed_at'; - else - v_order_by = 'name'; - end case; - - case - when sortorder = 'asc' then - v_sort_order = 'asc'; - when sortorder = 'desc' then - v_sort_order = 'desc'; - else - v_sort_order = 'asc'; - end case; - - v_order_by = v_order_by || ' ' || v_sort_order; - - return query execute - 'with folders as ( - select path_tokens[$1] as folder - from storage.objects - where objects.name ilike $2 || $3 || ''%'' - and bucket_id = $4 - and array_length(regexp_split_to_array(objects.name, ''/''), 1) <> $1 - group by folder - order by folder ' || v_sort_order || ' - ) - (select folder as "name", - null as id, - null as updated_at, - null as created_at, - null as last_accessed_at, - null as metadata from folders) - union all - (select path_tokens[$1] as "name", - id, - updated_at, - created_at, - last_accessed_at, - metadata - from storage.objects - where objects.name ilike $2 || $3 || ''%'' - and bucket_id = $4 - and array_length(regexp_split_to_array(objects.name, ''/''), 1) = $1 - order by ' || v_order_by || ') - limit $5 - offset $6' using levels, prefix, search, bucketname, limits, offsets; -end; -$_$; - - -ALTER FUNCTION storage.search(prefix text, bucketname text, limits integer, levels integer, offsets integer, search text, sortcolumn text, sortorder text) OWNER TO supabase_storage_admin; - --- --- Name: update_updated_at_column(); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin --- - -CREATE OR REPLACE FUNCTION storage.update_updated_at_column() RETURNS trigger - LANGUAGE plpgsql - AS $$ -BEGIN - NEW.updated_at = now(); - RETURN NEW; -END; -$$; - - -ALTER FUNCTION storage.update_updated_at_column() OWNER TO supabase_storage_admin; - -SET default_tablespace = ''; - -SET default_table_access_method = heap; - --- --- Name: extensions; Type: TABLE; Schema: _realtime; Owner: postgres --- - -CREATE TABLE IF NOT EXISTS _realtime.extensions ( - id uuid NOT NULL, - type text, - settings jsonb, - tenant_external_id text, - inserted_at timestamp(0) without time zone NOT NULL, - updated_at timestamp(0) without time zone NOT NULL -); - - -ALTER TABLE _realtime.extensions OWNER TO postgres; - --- --- Name: schema_migrations; Type: TABLE; Schema: _realtime; Owner: postgres --- - -CREATE TABLE IF NOT EXISTS _realtime.schema_migrations ( - version bigint NOT NULL, - inserted_at timestamp(0) without time zone -); - - -ALTER TABLE _realtime.schema_migrations OWNER TO postgres; - --- --- Name: tenants; Type: TABLE; Schema: _realtime; Owner: postgres --- - -CREATE TABLE IF NOT EXISTS _realtime.tenants ( - id uuid NOT NULL, - name text, - external_id text, - jwt_secret text, - max_concurrent_users integer DEFAULT 200 NOT NULL, - inserted_at timestamp(0) without time zone NOT NULL, - updated_at timestamp(0) without time zone NOT NULL, - max_events_per_second integer DEFAULT 100 NOT NULL, - postgres_cdc_default text DEFAULT 'postgres_cdc_rls'::text, - max_bytes_per_second integer DEFAULT 100000 NOT NULL, - max_channels_per_client integer DEFAULT 100 NOT NULL, - max_joins_per_second integer DEFAULT 500 NOT NULL, - suspend boolean DEFAULT false -); - - -ALTER TABLE _realtime.tenants OWNER TO postgres; - --- --- Name: audit_log_entries; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.audit_log_entries ( - instance_id uuid, - id uuid NOT NULL, - payload json, - created_at timestamp with time zone, - ip_address character varying(64) DEFAULT ''::character varying NOT NULL -); - - -ALTER TABLE auth.audit_log_entries OWNER TO supabase_auth_admin; - --- --- Name: TABLE audit_log_entries; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.audit_log_entries IS 'Auth: Audit trail for user actions.'; - - --- --- Name: flow_state; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.flow_state ( - id uuid NOT NULL, - user_id uuid, - auth_code text NOT NULL, - code_challenge_method auth.code_challenge_method NOT NULL, - code_challenge text NOT NULL, - provider_type text NOT NULL, - provider_access_token text, - provider_refresh_token text, - created_at timestamp with time zone, - updated_at timestamp with time zone, - authentication_method text NOT NULL -); - - -ALTER TABLE auth.flow_state OWNER TO supabase_auth_admin; - --- --- Name: TABLE flow_state; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.flow_state IS 'stores metadata for pkce logins'; - - --- --- Name: identities; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.identities ( - provider_id text NOT NULL, - user_id uuid NOT NULL, - identity_data jsonb NOT NULL, - provider text NOT NULL, - last_sign_in_at timestamp with time zone, - created_at timestamp with time zone, - updated_at timestamp with time zone, - email text GENERATED ALWAYS AS (lower((identity_data ->> 'email'::text))) STORED, - id uuid DEFAULT gen_random_uuid() NOT NULL -); - - -ALTER TABLE auth.identities OWNER TO supabase_auth_admin; - --- --- Name: TABLE identities; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.identities IS 'Auth: Stores identities associated to a user.'; - - --- --- Name: COLUMN identities.email; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON COLUMN auth.identities.email IS 'Auth: Email is a generated column that references the optional email property in the identity_data'; - - --- --- Name: instances; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.instances ( - id uuid NOT NULL, - uuid uuid, - raw_base_config text, - created_at timestamp with time zone, - updated_at timestamp with time zone -); - - -ALTER TABLE auth.instances OWNER TO supabase_auth_admin; - --- --- Name: TABLE instances; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.instances IS 'Auth: Manages users across multiple sites.'; - - --- --- Name: mfa_amr_claims; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.mfa_amr_claims ( - session_id uuid NOT NULL, - created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL, - authentication_method text NOT NULL, - id uuid NOT NULL -); - - -ALTER TABLE auth.mfa_amr_claims OWNER TO supabase_auth_admin; - --- --- Name: TABLE mfa_amr_claims; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.mfa_amr_claims IS 'auth: stores authenticator method reference claims for multi factor authentication'; - - --- --- Name: mfa_challenges; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.mfa_challenges ( - id uuid NOT NULL, - factor_id uuid NOT NULL, - created_at timestamp with time zone NOT NULL, - verified_at timestamp with time zone, - ip_address inet NOT NULL -); - - -ALTER TABLE auth.mfa_challenges OWNER TO supabase_auth_admin; - --- --- Name: TABLE mfa_challenges; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.mfa_challenges IS 'auth: stores metadata about challenge requests made'; - - --- --- Name: mfa_factors; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.mfa_factors ( - id uuid NOT NULL, - user_id uuid NOT NULL, - friendly_name text, - factor_type auth.factor_type NOT NULL, - status auth.factor_status NOT NULL, - created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL, - secret text -); - - -ALTER TABLE auth.mfa_factors OWNER TO supabase_auth_admin; - --- --- Name: TABLE mfa_factors; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.mfa_factors IS 'auth: stores metadata about factors'; - - --- --- Name: refresh_tokens; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.refresh_tokens ( - instance_id uuid, - id bigint NOT NULL, - token character varying(255), - user_id character varying(255), - revoked boolean, - created_at timestamp with time zone, - updated_at timestamp with time zone, - parent character varying(255), - session_id uuid -); - - -ALTER TABLE auth.refresh_tokens OWNER TO supabase_auth_admin; - --- --- Name: TABLE refresh_tokens; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.refresh_tokens IS 'Auth: Store of tokens used to refresh JWT tokens once they expire.'; - - --- --- Name: refresh_tokens_id_seq; Type: SEQUENCE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE SEQUENCE IF NOT EXISTS auth.refresh_tokens_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - -ALTER TABLE auth.refresh_tokens_id_seq OWNER TO supabase_auth_admin; - --- --- Name: refresh_tokens_id_seq; Type: SEQUENCE OWNED BY; Schema: auth; Owner: supabase_auth_admin --- - -ALTER SEQUENCE auth.refresh_tokens_id_seq OWNED BY auth.refresh_tokens.id; - - --- --- Name: saml_providers; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.saml_providers ( - id uuid NOT NULL, - sso_provider_id uuid NOT NULL, - entity_id text NOT NULL, - metadata_xml text NOT NULL, - metadata_url text, - attribute_mapping jsonb, - created_at timestamp with time zone, - updated_at timestamp with time zone, - CONSTRAINT "entity_id not empty" CHECK ((char_length(entity_id) > 0)), - CONSTRAINT "metadata_url not empty" CHECK (((metadata_url = NULL::text) OR (char_length(metadata_url) > 0))), - CONSTRAINT "metadata_xml not empty" CHECK ((char_length(metadata_xml) > 0)) -); - - -ALTER TABLE auth.saml_providers OWNER TO supabase_auth_admin; - --- --- Name: TABLE saml_providers; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.saml_providers IS 'Auth: Manages SAML Identity Provider connections.'; - - --- --- Name: saml_relay_states; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.saml_relay_states ( - id uuid NOT NULL, - sso_provider_id uuid NOT NULL, - request_id text NOT NULL, - for_email text, - redirect_to text, - from_ip_address inet, - created_at timestamp with time zone, - updated_at timestamp with time zone, - flow_state_id uuid, - CONSTRAINT "request_id not empty" CHECK ((char_length(request_id) > 0)) -); - - -ALTER TABLE auth.saml_relay_states OWNER TO supabase_auth_admin; - --- --- Name: TABLE saml_relay_states; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.saml_relay_states IS 'Auth: Contains SAML Relay State information for each Service Provider initiated login.'; - - --- --- Name: schema_migrations; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.schema_migrations ( - version character varying(255) NOT NULL -); - - -ALTER TABLE auth.schema_migrations OWNER TO supabase_auth_admin; - --- --- Name: TABLE schema_migrations; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.schema_migrations IS 'Auth: Manages updates to the auth system.'; - - --- --- Name: sessions; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.sessions ( - id uuid NOT NULL, - user_id uuid NOT NULL, - created_at timestamp with time zone, - updated_at timestamp with time zone, - factor_id uuid, - aal auth.aal_level, - not_after timestamp with time zone, - refreshed_at timestamp without time zone, - user_agent text, - ip inet, - tag text -); - - -ALTER TABLE auth.sessions OWNER TO supabase_auth_admin; - --- --- Name: TABLE sessions; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.sessions IS 'Auth: Stores session data associated to a user.'; - - --- --- Name: COLUMN sessions.not_after; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON COLUMN auth.sessions.not_after IS 'Auth: Not after is a nullable column that contains a timestamp after which the session should be regarded as expired.'; - - --- --- Name: sso_domains; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.sso_domains ( - id uuid NOT NULL, - sso_provider_id uuid NOT NULL, - domain text NOT NULL, - created_at timestamp with time zone, - updated_at timestamp with time zone, - CONSTRAINT "domain not empty" CHECK ((char_length(domain) > 0)) -); - - -ALTER TABLE auth.sso_domains OWNER TO supabase_auth_admin; - --- --- Name: TABLE sso_domains; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.sso_domains IS 'Auth: Manages SSO email address domain mapping to an SSO Identity Provider.'; - - --- --- Name: sso_providers; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.sso_providers ( - id uuid NOT NULL, - resource_id text, - created_at timestamp with time zone, - updated_at timestamp with time zone, - CONSTRAINT "resource_id not empty" CHECK (((resource_id = NULL::text) OR (char_length(resource_id) > 0))) -); - - -ALTER TABLE auth.sso_providers OWNER TO supabase_auth_admin; - --- --- Name: TABLE sso_providers; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.sso_providers IS 'Auth: Manages SSO identity provider information; see saml_providers for SAML.'; - - --- --- Name: COLUMN sso_providers.resource_id; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON COLUMN auth.sso_providers.resource_id IS 'Auth: Uniquely identifies a SSO provider according to a user-chosen resource ID (case insensitive), useful in infrastructure as code.'; - - --- --- Name: users; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.users ( - instance_id uuid, - id uuid NOT NULL, - aud character varying(255), - role character varying(255), - email character varying(255), - encrypted_password character varying(255), - email_confirmed_at timestamp with time zone, - invited_at timestamp with time zone, - confirmation_token character varying(255), - confirmation_sent_at timestamp with time zone, - recovery_token character varying(255), - recovery_sent_at timestamp with time zone, - email_change_token_new character varying(255), - email_change character varying(255), - email_change_sent_at timestamp with time zone, - last_sign_in_at timestamp with time zone, - raw_app_meta_data jsonb, - raw_user_meta_data jsonb, - is_super_admin boolean, - created_at timestamp with time zone, - updated_at timestamp with time zone, - phone text DEFAULT NULL::character varying, - phone_confirmed_at timestamp with time zone, - phone_change text DEFAULT ''::character varying, - phone_change_token character varying(255) DEFAULT ''::character varying, - phone_change_sent_at timestamp with time zone, - confirmed_at timestamp with time zone GENERATED ALWAYS AS (LEAST(email_confirmed_at, phone_confirmed_at)) STORED, - email_change_token_current character varying(255) DEFAULT ''::character varying, - email_change_confirm_status smallint DEFAULT 0, - banned_until timestamp with time zone, - reauthentication_token character varying(255) DEFAULT ''::character varying, - reauthentication_sent_at timestamp with time zone, - is_sso_user boolean DEFAULT false NOT NULL, - deleted_at timestamp with time zone, - CONSTRAINT users_email_change_confirm_status_check CHECK (((email_change_confirm_status >= 0) AND (email_change_confirm_status <= 2))) -); - - -ALTER TABLE auth.users OWNER TO supabase_auth_admin; - --- --- Name: TABLE users; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.users IS 'Auth: Stores user login data within a secure schema.'; - - --- --- Name: COLUMN users.is_sso_user; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON COLUMN auth.users.is_sso_user IS 'Auth: Set this column to true when the account comes from SSO. These accounts can have duplicate emails.'; - - --- --- Name: buckets; Type: TABLE; Schema: storage; Owner: supabase_storage_admin --- - -CREATE TABLE IF NOT EXISTS storage.buckets ( - id text NOT NULL, - name text NOT NULL, - owner uuid, - created_at timestamp with time zone DEFAULT now(), - updated_at timestamp with time zone DEFAULT now(), - public boolean DEFAULT false, - avif_autodetection boolean DEFAULT false, - file_size_limit bigint, - allowed_mime_types text[], - owner_id text -); - - -ALTER TABLE storage.buckets OWNER TO supabase_storage_admin; - --- --- Name: COLUMN buckets.owner; Type: COMMENT; Schema: storage; Owner: supabase_storage_admin --- - -COMMENT ON COLUMN storage.buckets.owner IS 'Field is deprecated, use owner_id instead'; - - --- --- Name: migrations; Type: TABLE; Schema: storage; Owner: supabase_storage_admin --- - -CREATE TABLE IF NOT EXISTS storage.migrations ( - id integer NOT NULL, - name character varying(100) NOT NULL, - hash character varying(40) NOT NULL, - executed_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP -); - - -ALTER TABLE storage.migrations OWNER TO supabase_storage_admin; - --- --- Name: objects; Type: TABLE; Schema: storage; Owner: supabase_storage_admin --- - -CREATE TABLE IF NOT EXISTS storage.objects ( - id uuid DEFAULT extensions.uuid_generate_v4() NOT NULL, - bucket_id text, - name text, - owner uuid, - created_at timestamp with time zone DEFAULT now(), - updated_at timestamp with time zone DEFAULT now(), - last_accessed_at timestamp with time zone DEFAULT now(), - metadata jsonb, - path_tokens text[] GENERATED ALWAYS AS (string_to_array(name, '/'::text)) STORED, - version text, - owner_id text -); - - -ALTER TABLE storage.objects OWNER TO supabase_storage_admin; - --- --- Name: COLUMN objects.owner; Type: COMMENT; Schema: storage; Owner: supabase_storage_admin --- - -COMMENT ON COLUMN storage.objects.owner IS 'Field is deprecated, use owner_id instead'; - - --- --- Name: refresh_tokens id; Type: DEFAULT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.refresh_tokens ALTER COLUMN id SET DEFAULT nextval('auth.refresh_tokens_id_seq'::regclass); - - --- --- Data for Name: extensions; Type: TABLE DATA; Schema: _realtime; Owner: postgres --- - -INSERT INTO _realtime.extensions (id, type, settings, tenant_external_id, inserted_at, updated_at) VALUES ('5518e2da-548e-499c-ab1c-664fd6446714', 'postgres_cdc_rls', '{"region": "us-east-1", "db_host": "ABK7kBu27y/PVdL10i/b+A==", "db_name": "sWBpZNdjggEPTQVlI52Zfw==", "db_port": "+enMDFi1J/3IrrquHHwUmA==", "db_user": "uxbEq/zz8DXVD53TOI1zmw==", "slot_name": "supabase_realtime_replication_slot", "ip_version": 4, "db_password": "sWBpZNdjggEPTQVlI52Zfw==", "publication": "supabase_realtime", "ssl_enforced": false, "poll_interval_ms": 100, "poll_max_changes": 100, "poll_max_record_bytes": 1048576}', 'realtime-dev', '2023-12-12 15:25:21', '2023-12-12 15:25:21'); - - --- --- Data for Name: schema_migrations; Type: TABLE DATA; Schema: _realtime; Owner: postgres --- - -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20210706140551, '2023-01-09 07:15:13'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20220329161857, '2023-01-09 07:15:13'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20220410212326, '2023-01-09 07:15:13'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20220506102948, '2023-01-09 07:15:13'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20220527210857, '2023-01-09 07:15:13'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20220815211129, '2023-01-09 07:15:13'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20220815215024, '2023-01-09 07:15:13'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20220818141501, '2023-01-09 07:15:13'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20221018173709, '2023-01-09 07:15:13'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20221102172703, '2023-01-09 07:15:13'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20221223010058, '2023-01-09 07:15:13'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20230110180046, '2023-04-06 09:25:28'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20230810220907, '2023-12-12 15:25:19'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20230810220924, '2023-12-12 15:25:19'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20231024094642, '2023-12-12 15:25:19'); - - --- --- Data for Name: tenants; Type: TABLE DATA; Schema: _realtime; Owner: postgres --- - -INSERT INTO _realtime.tenants (id, name, external_id, jwt_secret, max_concurrent_users, inserted_at, updated_at, max_events_per_second, postgres_cdc_default, max_bytes_per_second, max_channels_per_client, max_joins_per_second, suspend) VALUES ('e6edd492-6586-4c5d-9c2e-4e4de2e5c64d', 'realtime-dev', 'realtime-dev', 'iNjicxc4+llvc9wovDvqymwfnj9teWMlyOIbJ8Fh6j2WNU8CIJ2ZgjR6MUIKqSmeDmvpsKLsZ9jgXJmQPpwL8w==', 200, '2023-12-12 15:25:21', '2023-12-12 15:25:21', 100, 'postgres_cdc_rls', 100000, 100, 100, false); - - --- --- Data for Name: audit_log_entries; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: flow_state; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: identities; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: instances; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: mfa_amr_claims; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: mfa_challenges; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: mfa_factors; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: refresh_tokens; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: saml_providers; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: saml_relay_states; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: schema_migrations; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - -INSERT INTO auth.schema_migrations (version) VALUES ('20171026211738'); -INSERT INTO auth.schema_migrations (version) VALUES ('20171026211808'); -INSERT INTO auth.schema_migrations (version) VALUES ('20171026211834'); -INSERT INTO auth.schema_migrations (version) VALUES ('20180103212743'); -INSERT INTO auth.schema_migrations (version) VALUES ('20180108183307'); -INSERT INTO auth.schema_migrations (version) VALUES ('20180119214651'); -INSERT INTO auth.schema_migrations (version) VALUES ('20180125194653'); -INSERT INTO auth.schema_migrations (version) VALUES ('00'); -INSERT INTO auth.schema_migrations (version) VALUES ('20210710035447'); -INSERT INTO auth.schema_migrations (version) VALUES ('20210722035447'); -INSERT INTO auth.schema_migrations (version) VALUES ('20210730183235'); -INSERT INTO auth.schema_migrations (version) VALUES ('20210909172000'); -INSERT INTO auth.schema_migrations (version) VALUES ('20210927181326'); -INSERT INTO auth.schema_migrations (version) VALUES ('20211122151130'); -INSERT INTO auth.schema_migrations (version) VALUES ('20211124214934'); -INSERT INTO auth.schema_migrations (version) VALUES ('20211202183645'); -INSERT INTO auth.schema_migrations (version) VALUES ('20220114185221'); -INSERT INTO auth.schema_migrations (version) VALUES ('20220114185340'); -INSERT INTO auth.schema_migrations (version) VALUES ('20220224000811'); -INSERT INTO auth.schema_migrations (version) VALUES ('20220323170000'); -INSERT INTO auth.schema_migrations (version) VALUES ('20220429102000'); -INSERT INTO auth.schema_migrations (version) VALUES ('20220531120530'); -INSERT INTO auth.schema_migrations (version) VALUES ('20220614074223'); -INSERT INTO auth.schema_migrations (version) VALUES ('20220811173540'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221003041349'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221003041400'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221011041400'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221020193600'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221021073300'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221021082433'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221027105023'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221114143122'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221114143410'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221125140132'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221208132122'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221215195500'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221215195800'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221215195900'); -INSERT INTO auth.schema_migrations (version) VALUES ('20230116124310'); -INSERT INTO auth.schema_migrations (version) VALUES ('20230116124412'); -INSERT INTO auth.schema_migrations (version) VALUES ('20230131181311'); -INSERT INTO auth.schema_migrations (version) VALUES ('20230322519590'); -INSERT INTO auth.schema_migrations (version) VALUES ('20230402418590'); -INSERT INTO auth.schema_migrations (version) VALUES ('20230411005111'); -INSERT INTO auth.schema_migrations (version) VALUES ('20230508135423'); -INSERT INTO auth.schema_migrations (version) VALUES ('20230523124323'); -INSERT INTO auth.schema_migrations (version) VALUES ('20230818113222'); -INSERT INTO auth.schema_migrations (version) VALUES ('20230914180801'); -INSERT INTO auth.schema_migrations (version) VALUES ('20231027141322'); -INSERT INTO auth.schema_migrations (version) VALUES ('20231114161723'); -INSERT INTO auth.schema_migrations (version) VALUES ('20231117164230'); - - --- --- Data for Name: sessions; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: sso_domains; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: sso_providers; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: users; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: buckets; Type: TABLE DATA; Schema: storage; Owner: supabase_storage_admin --- - - - --- --- Data for Name: migrations; Type: TABLE DATA; Schema: storage; Owner: supabase_storage_admin --- - -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (0, 'create-migrations-table', 'e18db593bcde2aca2a408c4d1100f6abba2195df', '2022-04-14 09:52:39.214681'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (1, 'initialmigration', '6ab16121fbaa08bbd11b712d05f358f9b555d777', '2022-04-14 09:52:39.255149'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (2, 'pathtoken-column', '49756be03be4c17bb85fe70d4a861f27de7e49ad', '2022-04-14 09:52:39.276667'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (3, 'add-migrations-rls', 'bb5d124c53d68635a883e399426c6a5a25fc893d', '2022-04-14 09:52:39.587158'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (4, 'add-size-functions', '6d79007d04f5acd288c9c250c42d2d5fd286c54d', '2022-04-14 09:52:39.621747'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (5, 'change-column-name-in-get-size', 'fd65688505d2ffa9fbdc58a944348dd8604d688c', '2022-04-14 09:52:39.658796'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (6, 'add-rls-to-buckets', '63e2bab75a2040fee8e3fb3f15a0d26f3380e9b6', '2022-04-14 09:52:39.702011'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (7, 'add-public-to-buckets', '82568934f8a4d9e0a85f126f6fb483ad8214c418', '2022-04-14 09:52:39.739731'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (8, 'fix-search-function', '1a43a40eddb525f2e2f26efd709e6c06e58e059c', '2022-04-14 09:52:39.781062'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (9, 'search-files-search-function', '34c096597eb8b9d077fdfdde9878c88501b2fafc', '2022-08-23 04:48:07.728336'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (10, 'add-trigger-to-auto-update-updated_at-column', '37d6bb964a70a822e6d37f22f457b9bca7885928', '2022-08-23 04:48:07.736527'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (11, 'add-automatic-avif-detection-flag', 'bd76c53a9c564c80d98d119c1b3a28e16c8152db', '2023-04-06 09:25:29.356743'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (12, 'add-bucket-custom-limits', 'cbe0a4c32a0e891554a21020433b7a4423c07ee7', '2023-04-06 09:25:29.390303'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (13, 'use-bytes-for-max-size', '7a158ebce8a0c2801c9c65b7e9b2f98f68b3874e', '2023-04-06 09:25:29.402124'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (14, 'add-can-insert-object-function', '273193826bca7e0990b458d1ba72f8aa27c0d825', '2023-04-06 09:25:29.426206'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (15, 'add-version', 'e821a779d26612899b8c2dfe20245f904a327c4f', '2023-04-06 09:25:29.439561'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (16, 'drop-owner-foreign-key', '536b33f8878eed09d0144219777dcac96bdb25da', '2023-12-12 15:25:21.258059'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (17, 'add_owner_id_column_deprecate_owner', '7545f216a39358b5487df75d941d05dbcd75eb46', '2023-12-12 15:25:21.271667'); - - --- --- Data for Name: objects; Type: TABLE DATA; Schema: storage; Owner: supabase_storage_admin --- - - - --- --- Name: refresh_tokens_id_seq; Type: SEQUENCE SET; Schema: auth; Owner: supabase_auth_admin --- - -SELECT pg_catalog.setval('auth.refresh_tokens_id_seq', 1, false); - - --- --- Name: extensions extensions_pkey; Type: CONSTRAINT; Schema: _realtime; Owner: postgres --- - -ALTER TABLE ONLY _realtime.extensions - ADD CONSTRAINT extensions_pkey PRIMARY KEY (id); - - --- --- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: _realtime; Owner: postgres --- - -ALTER TABLE ONLY _realtime.schema_migrations - ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); - - --- --- Name: tenants tenants_pkey; Type: CONSTRAINT; Schema: _realtime; Owner: postgres --- - -ALTER TABLE ONLY _realtime.tenants - ADD CONSTRAINT tenants_pkey PRIMARY KEY (id); - - --- --- Name: mfa_amr_claims amr_id_pk; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.mfa_amr_claims - ADD CONSTRAINT amr_id_pk PRIMARY KEY (id); - - --- --- Name: audit_log_entries audit_log_entries_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.audit_log_entries - ADD CONSTRAINT audit_log_entries_pkey PRIMARY KEY (id); - - --- --- Name: flow_state flow_state_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.flow_state - ADD CONSTRAINT flow_state_pkey PRIMARY KEY (id); - - --- --- Name: identities identities_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.identities - ADD CONSTRAINT identities_pkey PRIMARY KEY (id); - - --- --- Name: identities identities_provider_id_provider_unique; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.identities - ADD CONSTRAINT identities_provider_id_provider_unique UNIQUE (provider_id, provider); - - --- --- Name: instances instances_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.instances - ADD CONSTRAINT instances_pkey PRIMARY KEY (id); - - --- --- Name: mfa_amr_claims mfa_amr_claims_session_id_authentication_method_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.mfa_amr_claims - ADD CONSTRAINT mfa_amr_claims_session_id_authentication_method_pkey UNIQUE (session_id, authentication_method); - - --- --- Name: mfa_challenges mfa_challenges_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.mfa_challenges - ADD CONSTRAINT mfa_challenges_pkey PRIMARY KEY (id); - - --- --- Name: mfa_factors mfa_factors_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.mfa_factors - ADD CONSTRAINT mfa_factors_pkey PRIMARY KEY (id); - - --- --- Name: refresh_tokens refresh_tokens_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.refresh_tokens - ADD CONSTRAINT refresh_tokens_pkey PRIMARY KEY (id); - - --- --- Name: refresh_tokens refresh_tokens_token_unique; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.refresh_tokens - ADD CONSTRAINT refresh_tokens_token_unique UNIQUE (token); - - --- --- Name: saml_providers saml_providers_entity_id_key; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.saml_providers - ADD CONSTRAINT saml_providers_entity_id_key UNIQUE (entity_id); - - --- --- Name: saml_providers saml_providers_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.saml_providers - ADD CONSTRAINT saml_providers_pkey PRIMARY KEY (id); - - --- --- Name: saml_relay_states saml_relay_states_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.saml_relay_states - ADD CONSTRAINT saml_relay_states_pkey PRIMARY KEY (id); - - --- --- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.schema_migrations - ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); - - --- --- Name: sessions sessions_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.sessions - ADD CONSTRAINT sessions_pkey PRIMARY KEY (id); - - --- --- Name: sso_domains sso_domains_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.sso_domains - ADD CONSTRAINT sso_domains_pkey PRIMARY KEY (id); - - --- --- Name: sso_providers sso_providers_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.sso_providers - ADD CONSTRAINT sso_providers_pkey PRIMARY KEY (id); - - --- --- Name: users users_phone_key; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.users - ADD CONSTRAINT users_phone_key UNIQUE (phone); - - --- --- Name: users users_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.users - ADD CONSTRAINT users_pkey PRIMARY KEY (id); - - --- --- Name: buckets buckets_pkey; Type: CONSTRAINT; Schema: storage; Owner: supabase_storage_admin --- - -ALTER TABLE ONLY storage.buckets - ADD CONSTRAINT buckets_pkey PRIMARY KEY (id); - - --- --- Name: migrations migrations_name_key; Type: CONSTRAINT; Schema: storage; Owner: supabase_storage_admin --- - -ALTER TABLE ONLY storage.migrations - ADD CONSTRAINT migrations_name_key UNIQUE (name); - - --- --- Name: migrations migrations_pkey; Type: CONSTRAINT; Schema: storage; Owner: supabase_storage_admin --- - -ALTER TABLE ONLY storage.migrations - ADD CONSTRAINT migrations_pkey PRIMARY KEY (id); - - --- --- Name: objects objects_pkey; Type: CONSTRAINT; Schema: storage; Owner: supabase_storage_admin --- - -ALTER TABLE ONLY storage.objects - ADD CONSTRAINT objects_pkey PRIMARY KEY (id); - - --- --- Name: extensions_tenant_external_id_type_index; Type: INDEX; Schema: _realtime; Owner: postgres --- - -CREATE UNIQUE INDEX extensions_tenant_external_id_type_index ON _realtime.extensions USING btree (tenant_external_id, type); - - --- --- Name: tenants_external_id_index; Type: INDEX; Schema: _realtime; Owner: postgres --- - -CREATE UNIQUE INDEX tenants_external_id_index ON _realtime.tenants USING btree (external_id); - - --- --- Name: audit_logs_instance_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX audit_logs_instance_id_idx ON auth.audit_log_entries USING btree (instance_id); - - --- --- Name: confirmation_token_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE UNIQUE INDEX confirmation_token_idx ON auth.users USING btree (confirmation_token) WHERE ((confirmation_token)::text !~ '^[0-9 ]*$'::text); - - --- --- Name: email_change_token_current_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE UNIQUE INDEX email_change_token_current_idx ON auth.users USING btree (email_change_token_current) WHERE ((email_change_token_current)::text !~ '^[0-9 ]*$'::text); - - --- --- Name: email_change_token_new_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE UNIQUE INDEX email_change_token_new_idx ON auth.users USING btree (email_change_token_new) WHERE ((email_change_token_new)::text !~ '^[0-9 ]*$'::text); - - --- --- Name: factor_id_created_at_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX factor_id_created_at_idx ON auth.mfa_factors USING btree (user_id, created_at); - - --- --- Name: flow_state_created_at_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX flow_state_created_at_idx ON auth.flow_state USING btree (created_at DESC); - - --- --- Name: identities_email_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX identities_email_idx ON auth.identities USING btree (email text_pattern_ops); - - --- --- Name: INDEX identities_email_idx; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON INDEX auth.identities_email_idx IS 'Auth: Ensures indexed queries on the email column'; - - --- --- Name: identities_user_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX identities_user_id_idx ON auth.identities USING btree (user_id); - - --- --- Name: idx_auth_code; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX idx_auth_code ON auth.flow_state USING btree (auth_code); - - --- --- Name: idx_user_id_auth_method; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX idx_user_id_auth_method ON auth.flow_state USING btree (user_id, authentication_method); - - --- --- Name: mfa_challenge_created_at_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX mfa_challenge_created_at_idx ON auth.mfa_challenges USING btree (created_at DESC); - - --- --- Name: mfa_factors_user_friendly_name_unique; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE UNIQUE INDEX mfa_factors_user_friendly_name_unique ON auth.mfa_factors USING btree (friendly_name, user_id) WHERE (btrim(friendly_name) <> ''::text); - - --- --- Name: mfa_factors_user_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX mfa_factors_user_id_idx ON auth.mfa_factors USING btree (user_id); - - --- --- Name: reauthentication_token_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE UNIQUE INDEX reauthentication_token_idx ON auth.users USING btree (reauthentication_token) WHERE ((reauthentication_token)::text !~ '^[0-9 ]*$'::text); - - --- --- Name: recovery_token_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE UNIQUE INDEX recovery_token_idx ON auth.users USING btree (recovery_token) WHERE ((recovery_token)::text !~ '^[0-9 ]*$'::text); - - --- --- Name: refresh_tokens_instance_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX refresh_tokens_instance_id_idx ON auth.refresh_tokens USING btree (instance_id); - - --- --- Name: refresh_tokens_instance_id_user_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX refresh_tokens_instance_id_user_id_idx ON auth.refresh_tokens USING btree (instance_id, user_id); - - --- --- Name: refresh_tokens_parent_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX refresh_tokens_parent_idx ON auth.refresh_tokens USING btree (parent); - - --- --- Name: refresh_tokens_session_id_revoked_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX refresh_tokens_session_id_revoked_idx ON auth.refresh_tokens USING btree (session_id, revoked); - - --- --- Name: refresh_tokens_updated_at_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX refresh_tokens_updated_at_idx ON auth.refresh_tokens USING btree (updated_at DESC); - - --- --- Name: saml_providers_sso_provider_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX saml_providers_sso_provider_id_idx ON auth.saml_providers USING btree (sso_provider_id); - - --- --- Name: saml_relay_states_created_at_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX saml_relay_states_created_at_idx ON auth.saml_relay_states USING btree (created_at DESC); - - --- --- Name: saml_relay_states_for_email_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX saml_relay_states_for_email_idx ON auth.saml_relay_states USING btree (for_email); - - --- --- Name: saml_relay_states_sso_provider_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX saml_relay_states_sso_provider_id_idx ON auth.saml_relay_states USING btree (sso_provider_id); - - --- --- Name: sessions_not_after_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX sessions_not_after_idx ON auth.sessions USING btree (not_after DESC); - - --- --- Name: sessions_user_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX sessions_user_id_idx ON auth.sessions USING btree (user_id); - - --- --- Name: sso_domains_domain_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE UNIQUE INDEX sso_domains_domain_idx ON auth.sso_domains USING btree (lower(domain)); - - --- --- Name: sso_domains_sso_provider_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX sso_domains_sso_provider_id_idx ON auth.sso_domains USING btree (sso_provider_id); - - --- --- Name: sso_providers_resource_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE UNIQUE INDEX sso_providers_resource_id_idx ON auth.sso_providers USING btree (lower(resource_id)); - - --- --- Name: user_id_created_at_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX user_id_created_at_idx ON auth.sessions USING btree (user_id, created_at); - - --- --- Name: users_email_partial_key; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE UNIQUE INDEX users_email_partial_key ON auth.users USING btree (email) WHERE (is_sso_user = false); - - --- --- Name: INDEX users_email_partial_key; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON INDEX auth.users_email_partial_key IS 'Auth: A partial unique index that applies only when is_sso_user is false'; - - --- --- Name: users_instance_id_email_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX users_instance_id_email_idx ON auth.users USING btree (instance_id, lower((email)::text)); - - --- --- Name: users_instance_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX users_instance_id_idx ON auth.users USING btree (instance_id); - - --- --- Name: bname; Type: INDEX; Schema: storage; Owner: supabase_storage_admin --- - -CREATE UNIQUE INDEX bname ON storage.buckets USING btree (name); - - --- --- Name: bucketid_objname; Type: INDEX; Schema: storage; Owner: supabase_storage_admin --- - -CREATE UNIQUE INDEX bucketid_objname ON storage.objects USING btree (bucket_id, name); - - --- --- Name: name_prefix_search; Type: INDEX; Schema: storage; Owner: supabase_storage_admin --- - -CREATE INDEX name_prefix_search ON storage.objects USING btree (name text_pattern_ops); - - --- --- Name: objects update_objects_updated_at; Type: TRIGGER; Schema: storage; Owner: supabase_storage_admin --- - -CREATE TRIGGER update_objects_updated_at BEFORE UPDATE ON storage.objects FOR EACH ROW EXECUTE FUNCTION storage.update_updated_at_column(); - - --- --- Name: extensions extensions_tenant_external_id_fkey; Type: FK CONSTRAINT; Schema: _realtime; Owner: postgres --- - -ALTER TABLE ONLY _realtime.extensions - ADD CONSTRAINT extensions_tenant_external_id_fkey FOREIGN KEY (tenant_external_id) REFERENCES _realtime.tenants(external_id) ON DELETE CASCADE; - - --- --- Name: identities identities_user_id_fkey; Type: FK CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.identities - ADD CONSTRAINT identities_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE; - - --- --- Name: mfa_amr_claims mfa_amr_claims_session_id_fkey; Type: FK CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.mfa_amr_claims - ADD CONSTRAINT mfa_amr_claims_session_id_fkey FOREIGN KEY (session_id) REFERENCES auth.sessions(id) ON DELETE CASCADE; - - --- --- Name: mfa_challenges mfa_challenges_auth_factor_id_fkey; Type: FK CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.mfa_challenges - ADD CONSTRAINT mfa_challenges_auth_factor_id_fkey FOREIGN KEY (factor_id) REFERENCES auth.mfa_factors(id) ON DELETE CASCADE; - - --- --- Name: mfa_factors mfa_factors_user_id_fkey; Type: FK CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.mfa_factors - ADD CONSTRAINT mfa_factors_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE; - - --- --- Name: refresh_tokens refresh_tokens_session_id_fkey; Type: FK CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.refresh_tokens - ADD CONSTRAINT refresh_tokens_session_id_fkey FOREIGN KEY (session_id) REFERENCES auth.sessions(id) ON DELETE CASCADE; - - --- --- Name: saml_providers saml_providers_sso_provider_id_fkey; Type: FK CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.saml_providers - ADD CONSTRAINT saml_providers_sso_provider_id_fkey FOREIGN KEY (sso_provider_id) REFERENCES auth.sso_providers(id) ON DELETE CASCADE; - - --- --- Name: saml_relay_states saml_relay_states_flow_state_id_fkey; Type: FK CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.saml_relay_states - ADD CONSTRAINT saml_relay_states_flow_state_id_fkey FOREIGN KEY (flow_state_id) REFERENCES auth.flow_state(id) ON DELETE CASCADE; - - --- --- Name: saml_relay_states saml_relay_states_sso_provider_id_fkey; Type: FK CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.saml_relay_states - ADD CONSTRAINT saml_relay_states_sso_provider_id_fkey FOREIGN KEY (sso_provider_id) REFERENCES auth.sso_providers(id) ON DELETE CASCADE; - - --- --- Name: sessions sessions_user_id_fkey; Type: FK CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.sessions - ADD CONSTRAINT sessions_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE; - - --- --- Name: sso_domains sso_domains_sso_provider_id_fkey; Type: FK CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.sso_domains - ADD CONSTRAINT sso_domains_sso_provider_id_fkey FOREIGN KEY (sso_provider_id) REFERENCES auth.sso_providers(id) ON DELETE CASCADE; - - --- --- Name: objects objects_bucketId_fkey; Type: FK CONSTRAINT; Schema: storage; Owner: supabase_storage_admin --- - -ALTER TABLE ONLY storage.objects - ADD CONSTRAINT "objects_bucketId_fkey" FOREIGN KEY (bucket_id) REFERENCES storage.buckets(id); - - --- --- Name: buckets; Type: ROW SECURITY; Schema: storage; Owner: supabase_storage_admin --- - -ALTER TABLE storage.buckets ENABLE ROW LEVEL SECURITY; - --- --- Name: migrations; Type: ROW SECURITY; Schema: storage; Owner: supabase_storage_admin --- - -ALTER TABLE storage.migrations ENABLE ROW LEVEL SECURITY; - --- --- Name: objects; Type: ROW SECURITY; Schema: storage; Owner: supabase_storage_admin --- - -ALTER TABLE storage.objects ENABLE ROW LEVEL SECURITY; - --- --- Name: supabase_realtime; Type: PUBLICATION; Schema: -; Owner: postgres --- - -CREATE PUBLICATION supabase_realtime WITH (publish = 'insert, update, delete, truncate'); - - -ALTER PUBLICATION supabase_realtime OWNER TO postgres; - --- --- Name: SCHEMA auth; Type: ACL; Schema: -; Owner: supabase_admin --- - -GRANT USAGE ON SCHEMA auth TO anon; -GRANT USAGE ON SCHEMA auth TO authenticated; -GRANT USAGE ON SCHEMA auth TO service_role; -GRANT ALL ON SCHEMA auth TO supabase_auth_admin; -GRANT ALL ON SCHEMA auth TO dashboard_user; -GRANT ALL ON SCHEMA auth TO postgres; - - --- --- Name: SCHEMA extensions; Type: ACL; Schema: -; Owner: postgres --- - -GRANT USAGE ON SCHEMA extensions TO anon; -GRANT USAGE ON SCHEMA extensions TO authenticated; -GRANT USAGE ON SCHEMA extensions TO service_role; -GRANT ALL ON SCHEMA extensions TO dashboard_user; - - --- --- Name: SCHEMA graphql_public; Type: ACL; Schema: -; Owner: supabase_admin --- - -GRANT USAGE ON SCHEMA graphql_public TO postgres; -GRANT USAGE ON SCHEMA graphql_public TO anon; -GRANT USAGE ON SCHEMA graphql_public TO authenticated; -GRANT USAGE ON SCHEMA graphql_public TO service_role; - - --- --- Name: SCHEMA public; Type: ACL; Schema: -; Owner: postgres --- - -REVOKE USAGE ON SCHEMA public FROM PUBLIC; -GRANT ALL ON SCHEMA public TO PUBLIC; -GRANT USAGE ON SCHEMA public TO anon; -GRANT USAGE ON SCHEMA public TO authenticated; -GRANT USAGE ON SCHEMA public TO service_role; - - --- --- Name: SCHEMA realtime; Type: ACL; Schema: -; Owner: supabase_admin --- - -GRANT USAGE ON SCHEMA realtime TO postgres; - - --- --- Name: SCHEMA storage; Type: ACL; Schema: -; Owner: supabase_admin --- - -GRANT ALL ON SCHEMA storage TO postgres; -GRANT USAGE ON SCHEMA storage TO anon; -GRANT USAGE ON SCHEMA storage TO authenticated; -GRANT USAGE ON SCHEMA storage TO service_role; -GRANT ALL ON SCHEMA storage TO supabase_storage_admin; -GRANT ALL ON SCHEMA storage TO dashboard_user; - - --- --- Name: FUNCTION email(); Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON FUNCTION auth.email() TO dashboard_user; - - --- --- Name: FUNCTION jwt(); Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON FUNCTION auth.jwt() TO postgres; -GRANT ALL ON FUNCTION auth.jwt() TO dashboard_user; - - --- --- Name: FUNCTION role(); Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON FUNCTION auth.role() TO dashboard_user; - - --- --- Name: FUNCTION uid(); Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON FUNCTION auth.uid() TO dashboard_user; - - --- --- Name: FUNCTION algorithm_sign(signables text, secret text, algorithm text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.algorithm_sign(signables text, secret text, algorithm text) TO dashboard_user; - - --- --- Name: FUNCTION armor(bytea); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.armor(bytea) TO dashboard_user; - - --- --- Name: FUNCTION armor(bytea, text[], text[]); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.armor(bytea, text[], text[]) TO dashboard_user; - - --- --- Name: FUNCTION crypt(text, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.crypt(text, text) TO dashboard_user; - - --- --- Name: FUNCTION dearmor(text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.dearmor(text) TO dashboard_user; - - --- --- Name: FUNCTION decrypt(bytea, bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.decrypt(bytea, bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION decrypt_iv(bytea, bytea, bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.decrypt_iv(bytea, bytea, bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION digest(bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.digest(bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION digest(text, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.digest(text, text) TO dashboard_user; - - --- --- Name: FUNCTION encrypt(bytea, bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.encrypt(bytea, bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION encrypt_iv(bytea, bytea, bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.encrypt_iv(bytea, bytea, bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION gen_random_bytes(integer); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.gen_random_bytes(integer) TO dashboard_user; - - --- --- Name: FUNCTION gen_random_uuid(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.gen_random_uuid() TO dashboard_user; - - --- --- Name: FUNCTION gen_salt(text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.gen_salt(text) TO dashboard_user; - - --- --- Name: FUNCTION gen_salt(text, integer); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.gen_salt(text, integer) TO dashboard_user; - - --- --- Name: FUNCTION grant_pg_cron_access(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.grant_pg_cron_access() TO dashboard_user; - - --- --- Name: FUNCTION grant_pg_net_access(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.grant_pg_net_access() TO dashboard_user; - - --- --- Name: FUNCTION hmac(bytea, bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.hmac(bytea, bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION hmac(text, text, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.hmac(text, text, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_armor_headers(text, OUT key text, OUT value text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_armor_headers(text, OUT key text, OUT value text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_key_id(bytea); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_key_id(bytea) TO dashboard_user; - - --- --- Name: FUNCTION pgp_pub_decrypt(bytea, bytea); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_pub_decrypt(bytea, bytea) TO dashboard_user; - - --- --- Name: FUNCTION pgp_pub_decrypt(bytea, bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_pub_decrypt(bytea, bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_pub_decrypt(bytea, bytea, text, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_pub_decrypt(bytea, bytea, text, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_pub_decrypt_bytea(bytea, bytea); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_pub_decrypt_bytea(bytea, bytea) TO dashboard_user; - - --- --- Name: FUNCTION pgp_pub_decrypt_bytea(bytea, bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_pub_decrypt_bytea(bytea, bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_pub_decrypt_bytea(bytea, bytea, text, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_pub_decrypt_bytea(bytea, bytea, text, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_pub_encrypt(text, bytea); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_pub_encrypt(text, bytea) TO dashboard_user; - - --- --- Name: FUNCTION pgp_pub_encrypt(text, bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_pub_encrypt(text, bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_pub_encrypt_bytea(bytea, bytea); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_pub_encrypt_bytea(bytea, bytea) TO dashboard_user; - - --- --- Name: FUNCTION pgp_pub_encrypt_bytea(bytea, bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_pub_encrypt_bytea(bytea, bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_sym_decrypt(bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_sym_decrypt(bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_sym_decrypt(bytea, text, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_sym_decrypt(bytea, text, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_sym_decrypt_bytea(bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_sym_decrypt_bytea(bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_sym_decrypt_bytea(bytea, text, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_sym_decrypt_bytea(bytea, text, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_sym_encrypt(text, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_sym_encrypt(text, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_sym_encrypt(text, text, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_sym_encrypt(text, text, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_sym_encrypt_bytea(bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_sym_encrypt_bytea(bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_sym_encrypt_bytea(bytea, text, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_sym_encrypt_bytea(bytea, text, text) TO dashboard_user; - - --- --- Name: FUNCTION sign(payload json, secret text, algorithm text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.sign(payload json, secret text, algorithm text) TO dashboard_user; - - --- --- Name: FUNCTION url_decode(data text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.url_decode(data text) TO dashboard_user; - - --- --- Name: FUNCTION url_encode(data bytea); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.url_encode(data bytea) TO dashboard_user; - - --- --- Name: FUNCTION uuid_generate_v1(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.uuid_generate_v1() TO dashboard_user; - - --- --- Name: FUNCTION uuid_generate_v1mc(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.uuid_generate_v1mc() TO dashboard_user; - - --- --- Name: FUNCTION uuid_generate_v3(namespace uuid, name text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.uuid_generate_v3(namespace uuid, name text) TO dashboard_user; - - --- --- Name: FUNCTION uuid_generate_v4(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.uuid_generate_v4() TO dashboard_user; - - --- --- Name: FUNCTION uuid_generate_v5(namespace uuid, name text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.uuid_generate_v5(namespace uuid, name text) TO dashboard_user; - - --- --- Name: FUNCTION uuid_nil(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.uuid_nil() TO dashboard_user; - - --- --- Name: FUNCTION uuid_ns_dns(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.uuid_ns_dns() TO dashboard_user; - - --- --- Name: FUNCTION uuid_ns_oid(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.uuid_ns_oid() TO dashboard_user; - - --- --- Name: FUNCTION uuid_ns_url(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.uuid_ns_url() TO dashboard_user; - - --- --- Name: FUNCTION uuid_ns_x500(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.uuid_ns_x500() TO dashboard_user; - - --- --- Name: FUNCTION verify(token text, secret text, algorithm text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.verify(token text, secret text, algorithm text) TO dashboard_user; - - --- --- Name: FUNCTION extension(name text); Type: ACL; Schema: storage; Owner: supabase_storage_admin --- - -GRANT ALL ON FUNCTION storage.extension(name text) TO anon; -GRANT ALL ON FUNCTION storage.extension(name text) TO authenticated; -GRANT ALL ON FUNCTION storage.extension(name text) TO service_role; -GRANT ALL ON FUNCTION storage.extension(name text) TO dashboard_user; -GRANT ALL ON FUNCTION storage.extension(name text) TO postgres; - - --- --- Name: FUNCTION filename(name text); Type: ACL; Schema: storage; Owner: supabase_storage_admin --- - -GRANT ALL ON FUNCTION storage.filename(name text) TO anon; -GRANT ALL ON FUNCTION storage.filename(name text) TO authenticated; -GRANT ALL ON FUNCTION storage.filename(name text) TO service_role; -GRANT ALL ON FUNCTION storage.filename(name text) TO dashboard_user; -GRANT ALL ON FUNCTION storage.filename(name text) TO postgres; - - --- --- Name: FUNCTION foldername(name text); Type: ACL; Schema: storage; Owner: supabase_storage_admin --- - -GRANT ALL ON FUNCTION storage.foldername(name text) TO anon; -GRANT ALL ON FUNCTION storage.foldername(name text) TO authenticated; -GRANT ALL ON FUNCTION storage.foldername(name text) TO service_role; -GRANT ALL ON FUNCTION storage.foldername(name text) TO dashboard_user; -GRANT ALL ON FUNCTION storage.foldername(name text) TO postgres; - - --- --- Name: TABLE audit_log_entries; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.audit_log_entries TO dashboard_user; -GRANT ALL ON TABLE auth.audit_log_entries TO postgres; - - --- --- Name: TABLE flow_state; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.flow_state TO postgres; -GRANT ALL ON TABLE auth.flow_state TO dashboard_user; - - --- --- Name: TABLE identities; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.identities TO postgres; -GRANT ALL ON TABLE auth.identities TO dashboard_user; - - --- --- Name: TABLE instances; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.instances TO dashboard_user; -GRANT ALL ON TABLE auth.instances TO postgres; - - --- --- Name: TABLE mfa_amr_claims; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.mfa_amr_claims TO postgres; -GRANT ALL ON TABLE auth.mfa_amr_claims TO dashboard_user; - - --- --- Name: TABLE mfa_challenges; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.mfa_challenges TO postgres; -GRANT ALL ON TABLE auth.mfa_challenges TO dashboard_user; - - --- --- Name: TABLE mfa_factors; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.mfa_factors TO postgres; -GRANT ALL ON TABLE auth.mfa_factors TO dashboard_user; - - --- --- Name: TABLE refresh_tokens; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.refresh_tokens TO dashboard_user; -GRANT ALL ON TABLE auth.refresh_tokens TO postgres; - - --- --- Name: SEQUENCE refresh_tokens_id_seq; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON SEQUENCE auth.refresh_tokens_id_seq TO dashboard_user; -GRANT ALL ON SEQUENCE auth.refresh_tokens_id_seq TO postgres; - - --- --- Name: TABLE saml_providers; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.saml_providers TO postgres; -GRANT ALL ON TABLE auth.saml_providers TO dashboard_user; - - --- --- Name: TABLE saml_relay_states; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.saml_relay_states TO postgres; -GRANT ALL ON TABLE auth.saml_relay_states TO dashboard_user; - - --- --- Name: TABLE schema_migrations; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.schema_migrations TO dashboard_user; -GRANT ALL ON TABLE auth.schema_migrations TO postgres; - - --- --- Name: TABLE sessions; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.sessions TO postgres; -GRANT ALL ON TABLE auth.sessions TO dashboard_user; - - --- --- Name: TABLE sso_domains; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.sso_domains TO postgres; -GRANT ALL ON TABLE auth.sso_domains TO dashboard_user; - - --- --- Name: TABLE sso_providers; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.sso_providers TO postgres; -GRANT ALL ON TABLE auth.sso_providers TO dashboard_user; - - --- --- Name: TABLE users; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.users TO dashboard_user; -GRANT ALL ON TABLE auth.users TO postgres; - - --- --- Name: TABLE buckets; Type: ACL; Schema: storage; Owner: supabase_storage_admin --- - -GRANT ALL ON TABLE storage.buckets TO anon; -GRANT ALL ON TABLE storage.buckets TO authenticated; -GRANT ALL ON TABLE storage.buckets TO service_role; -GRANT ALL ON TABLE storage.buckets TO postgres; - - --- --- Name: TABLE migrations; Type: ACL; Schema: storage; Owner: supabase_storage_admin --- - -GRANT ALL ON TABLE storage.migrations TO anon; -GRANT ALL ON TABLE storage.migrations TO authenticated; -GRANT ALL ON TABLE storage.migrations TO service_role; -GRANT ALL ON TABLE storage.migrations TO postgres; - - --- --- Name: TABLE objects; Type: ACL; Schema: storage; Owner: supabase_storage_admin --- - -GRANT ALL ON TABLE storage.objects TO anon; -GRANT ALL ON TABLE storage.objects TO authenticated; -GRANT ALL ON TABLE storage.objects TO service_role; -GRANT ALL ON TABLE storage.objects TO postgres; - - --- --- Name: DEFAULT PRIVILEGES FOR SEQUENCES; Type: DEFAULT ACL; Schema: auth; Owner: supabase_auth_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_auth_admin IN SCHEMA auth GRANT ALL ON SEQUENCES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_auth_admin IN SCHEMA auth GRANT ALL ON SEQUENCES TO dashboard_user; - - --- --- Name: DEFAULT PRIVILEGES FOR FUNCTIONS; Type: DEFAULT ACL; Schema: auth; Owner: supabase_auth_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_auth_admin IN SCHEMA auth GRANT ALL ON FUNCTIONS TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_auth_admin IN SCHEMA auth GRANT ALL ON FUNCTIONS TO dashboard_user; - - --- --- Name: DEFAULT PRIVILEGES FOR TABLES; Type: DEFAULT ACL; Schema: auth; Owner: supabase_auth_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_auth_admin IN SCHEMA auth GRANT ALL ON TABLES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_auth_admin IN SCHEMA auth GRANT ALL ON TABLES TO dashboard_user; - - --- --- Name: DEFAULT PRIVILEGES FOR SEQUENCES; Type: DEFAULT ACL; Schema: graphql_public; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON SEQUENCES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON SEQUENCES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON SEQUENCES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON SEQUENCES TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR FUNCTIONS; Type: DEFAULT ACL; Schema: graphql_public; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON FUNCTIONS TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON FUNCTIONS TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON FUNCTIONS TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON FUNCTIONS TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR TABLES; Type: DEFAULT ACL; Schema: graphql_public; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON TABLES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON TABLES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON TABLES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON TABLES TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR SEQUENCES; Type: DEFAULT ACL; Schema: public; Owner: postgres --- - -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR SEQUENCES; Type: DEFAULT ACL; Schema: public; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR FUNCTIONS; Type: DEFAULT ACL; Schema: public; Owner: postgres --- - -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR FUNCTIONS; Type: DEFAULT ACL; Schema: public; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR TABLES; Type: DEFAULT ACL; Schema: public; Owner: postgres --- - -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR TABLES; Type: DEFAULT ACL; Schema: public; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR SEQUENCES; Type: DEFAULT ACL; Schema: realtime; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA realtime GRANT ALL ON SEQUENCES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA realtime GRANT ALL ON SEQUENCES TO dashboard_user; - - --- --- Name: DEFAULT PRIVILEGES FOR FUNCTIONS; Type: DEFAULT ACL; Schema: realtime; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA realtime GRANT ALL ON FUNCTIONS TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA realtime GRANT ALL ON FUNCTIONS TO dashboard_user; - - --- --- Name: DEFAULT PRIVILEGES FOR TABLES; Type: DEFAULT ACL; Schema: realtime; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA realtime GRANT ALL ON TABLES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA realtime GRANT ALL ON TABLES TO dashboard_user; - - --- --- Name: DEFAULT PRIVILEGES FOR SEQUENCES; Type: DEFAULT ACL; Schema: storage; Owner: postgres --- - -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON SEQUENCES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON SEQUENCES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON SEQUENCES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON SEQUENCES TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR FUNCTIONS; Type: DEFAULT ACL; Schema: storage; Owner: postgres --- - -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON FUNCTIONS TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON FUNCTIONS TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON FUNCTIONS TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON FUNCTIONS TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR TABLES; Type: DEFAULT ACL; Schema: storage; Owner: postgres --- - -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON TABLES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON TABLES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON TABLES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON TABLES TO service_role; - - --- --- Name: issue_graphql_placeholder; Type: EVENT TRIGGER; Schema: -; Owner: supabase_admin --- - -CREATE EVENT TRIGGER issue_graphql_placeholder ON sql_drop - WHEN TAG IN ('DROP EXTENSION') - EXECUTE FUNCTION extensions.set_graphql_placeholder(); - - -ALTER EVENT TRIGGER issue_graphql_placeholder OWNER TO supabase_admin; - --- --- Name: issue_pg_cron_access; Type: EVENT TRIGGER; Schema: -; Owner: supabase_admin --- - -CREATE EVENT TRIGGER issue_pg_cron_access ON ddl_command_end - WHEN TAG IN ('CREATE SCHEMA') - EXECUTE FUNCTION extensions.grant_pg_cron_access(); - - -ALTER EVENT TRIGGER issue_pg_cron_access OWNER TO supabase_admin; - --- --- Name: issue_pg_graphql_access; Type: EVENT TRIGGER; Schema: -; Owner: supabase_admin --- - -CREATE EVENT TRIGGER issue_pg_graphql_access ON ddl_command_end - WHEN TAG IN ('CREATE FUNCTION') - EXECUTE FUNCTION extensions.grant_pg_graphql_access(); - - -ALTER EVENT TRIGGER issue_pg_graphql_access OWNER TO supabase_admin; - --- --- Name: issue_pg_net_access; Type: EVENT TRIGGER; Schema: -; Owner: supabase_admin --- - -CREATE EVENT TRIGGER issue_pg_net_access ON ddl_command_end - WHEN TAG IN ('CREATE EXTENSION') - EXECUTE FUNCTION extensions.grant_pg_net_access(); - - -ALTER EVENT TRIGGER issue_pg_net_access OWNER TO supabase_admin; - --- --- Name: pgrst_ddl_watch; Type: EVENT TRIGGER; Schema: -; Owner: supabase_admin --- - -CREATE EVENT TRIGGER pgrst_ddl_watch ON ddl_command_end - EXECUTE FUNCTION extensions.pgrst_ddl_watch(); - - -ALTER EVENT TRIGGER pgrst_ddl_watch OWNER TO supabase_admin; - --- --- Name: pgrst_drop_watch; Type: EVENT TRIGGER; Schema: -; Owner: supabase_admin --- - -CREATE EVENT TRIGGER pgrst_drop_watch ON sql_drop - EXECUTE FUNCTION extensions.pgrst_drop_watch(); - - -ALTER EVENT TRIGGER pgrst_drop_watch OWNER TO supabase_admin; - --- --- PostgreSQL database dump complete --- - -RESET ALL; diff --git a/apps/cli-go/internal/utils/templates/initial_schemas/14.sql b/apps/cli-go/internal/utils/templates/initial_schemas/14.sql deleted file mode 100644 index bef44153ec..0000000000 --- a/apps/cli-go/internal/utils/templates/initial_schemas/14.sql +++ /dev/null @@ -1,3690 +0,0 @@ --- --- PostgreSQL database dump --- - --- Dumped from database version 14.5 (Debian 14.5-2.pgdg110+2) --- Dumped by pg_dump version 15.3 - -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - --- --- Name: _analytics; Type: SCHEMA; Schema: -; Owner: postgres --- - -CREATE SCHEMA IF NOT EXISTS _analytics; - - -ALTER SCHEMA _analytics OWNER TO postgres; - --- --- Name: _realtime; Type: SCHEMA; Schema: -; Owner: postgres --- - -CREATE SCHEMA IF NOT EXISTS _realtime; - - -ALTER SCHEMA _realtime OWNER TO postgres; - --- --- Name: auth; Type: SCHEMA; Schema: -; Owner: supabase_admin --- - -CREATE SCHEMA IF NOT EXISTS auth; - - -ALTER SCHEMA auth OWNER TO supabase_admin; - --- --- Name: extensions; Type: SCHEMA; Schema: -; Owner: postgres --- - -CREATE SCHEMA IF NOT EXISTS extensions; - - -ALTER SCHEMA extensions OWNER TO postgres; - --- --- Name: graphql; Type: SCHEMA; Schema: -; Owner: supabase_admin --- - -CREATE SCHEMA IF NOT EXISTS graphql; - - -ALTER SCHEMA graphql OWNER TO supabase_admin; - --- --- Name: graphql_public; Type: SCHEMA; Schema: -; Owner: supabase_admin --- - -CREATE SCHEMA IF NOT EXISTS graphql_public; - - -ALTER SCHEMA graphql_public OWNER TO supabase_admin; - --- --- Name: pg_net; Type: EXTENSION; Schema: -; Owner: - --- - -CREATE EXTENSION IF NOT EXISTS pg_net WITH SCHEMA extensions; - - --- --- Name: EXTENSION pg_net; Type: COMMENT; Schema: -; Owner: --- - -COMMENT ON EXTENSION pg_net IS 'Async HTTP'; - - --- --- Name: pgbouncer; Type: SCHEMA; Schema: -; Owner: pgbouncer --- - -CREATE SCHEMA IF NOT EXISTS pgbouncer; - - -ALTER SCHEMA pgbouncer OWNER TO pgbouncer; - --- --- Name: pgsodium; Type: SCHEMA; Schema: -; Owner: postgres --- - -CREATE SCHEMA IF NOT EXISTS pgsodium; - - -ALTER SCHEMA pgsodium OWNER TO postgres; - --- --- Name: pgsodium; Type: EXTENSION; Schema: -; Owner: - --- - -CREATE EXTENSION IF NOT EXISTS pgsodium WITH SCHEMA pgsodium; - - --- --- Name: EXTENSION pgsodium; Type: COMMENT; Schema: -; Owner: --- - -COMMENT ON EXTENSION pgsodium IS 'Pgsodium is a modern cryptography library for Postgres.'; - - --- --- Name: public; Type: SCHEMA; Schema: -; Owner: postgres --- - --- *not* creating schema, since initdb creates it - - -ALTER SCHEMA public OWNER TO postgres; - --- --- Name: realtime; Type: SCHEMA; Schema: -; Owner: supabase_admin --- - -CREATE SCHEMA IF NOT EXISTS realtime; - - -ALTER SCHEMA realtime OWNER TO supabase_admin; - --- --- Name: storage; Type: SCHEMA; Schema: -; Owner: supabase_admin --- - -CREATE SCHEMA IF NOT EXISTS storage; - - -ALTER SCHEMA storage OWNER TO supabase_admin; - --- --- Name: supabase_functions; Type: SCHEMA; Schema: -; Owner: supabase_admin --- - -CREATE SCHEMA IF NOT EXISTS supabase_functions; - - -ALTER SCHEMA supabase_functions OWNER TO supabase_admin; - --- --- Name: pg_graphql; Type: EXTENSION; Schema: -; Owner: - --- - -CREATE EXTENSION IF NOT EXISTS pg_graphql WITH SCHEMA graphql; - - --- --- Name: EXTENSION pg_graphql; Type: COMMENT; Schema: -; Owner: --- - -COMMENT ON EXTENSION pg_graphql IS 'pg_graphql: GraphQL support'; - - --- --- Name: pg_stat_statements; Type: EXTENSION; Schema: -; Owner: - --- - -CREATE EXTENSION IF NOT EXISTS pg_stat_statements WITH SCHEMA extensions; - - --- --- Name: EXTENSION pg_stat_statements; Type: COMMENT; Schema: -; Owner: --- - -COMMENT ON EXTENSION pg_stat_statements IS 'track planning and execution statistics of all SQL statements executed'; - - --- --- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: - --- - -CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA extensions; - - --- --- Name: EXTENSION pgcrypto; Type: COMMENT; Schema: -; Owner: --- - -COMMENT ON EXTENSION pgcrypto IS 'cryptographic functions'; - - --- --- Name: pgjwt; Type: EXTENSION; Schema: -; Owner: - --- - -CREATE EXTENSION IF NOT EXISTS pgjwt WITH SCHEMA extensions; - - --- --- Name: EXTENSION pgjwt; Type: COMMENT; Schema: -; Owner: --- - -COMMENT ON EXTENSION pgjwt IS 'JSON Web Token API for Postgresql'; - - --- --- Name: uuid-ossp; Type: EXTENSION; Schema: -; Owner: - --- - -CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA extensions; - - --- --- Name: EXTENSION "uuid-ossp"; Type: COMMENT; Schema: -; Owner: --- - -COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UUIDs)'; - - --- --- Name: aal_level; Type: TYPE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TYPE auth.aal_level AS ENUM ( - 'aal1', - 'aal2', - 'aal3' -); - - -ALTER TYPE auth.aal_level OWNER TO supabase_auth_admin; - --- --- Name: code_challenge_method; Type: TYPE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TYPE auth.code_challenge_method AS ENUM ( - 's256', - 'plain' -); - - -ALTER TYPE auth.code_challenge_method OWNER TO supabase_auth_admin; - --- --- Name: factor_status; Type: TYPE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TYPE auth.factor_status AS ENUM ( - 'unverified', - 'verified' -); - - -ALTER TYPE auth.factor_status OWNER TO supabase_auth_admin; - --- --- Name: factor_type; Type: TYPE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TYPE auth.factor_type AS ENUM ( - 'totp', - 'webauthn' -); - - -ALTER TYPE auth.factor_type OWNER TO supabase_auth_admin; - --- --- Name: email(); Type: FUNCTION; Schema: auth; Owner: supabase_auth_admin --- - -CREATE OR REPLACE FUNCTION auth.email() RETURNS text - LANGUAGE sql STABLE - AS $$ - select - coalesce( - nullif(current_setting('request.jwt.claim.email', true), ''), - (nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'email') - )::text -$$; - - -ALTER FUNCTION auth.email() OWNER TO supabase_auth_admin; - --- --- Name: FUNCTION email(); Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON FUNCTION auth.email() IS 'Deprecated. Use auth.jwt() -> ''email'' instead.'; - - --- --- Name: jwt(); Type: FUNCTION; Schema: auth; Owner: supabase_auth_admin --- - -CREATE OR REPLACE FUNCTION auth.jwt() RETURNS jsonb - LANGUAGE sql STABLE - AS $$ - select - coalesce( - nullif(current_setting('request.jwt.claim', true), ''), - nullif(current_setting('request.jwt.claims', true), '') - )::jsonb -$$; - - -ALTER FUNCTION auth.jwt() OWNER TO supabase_auth_admin; - --- --- Name: role(); Type: FUNCTION; Schema: auth; Owner: supabase_auth_admin --- - -CREATE OR REPLACE FUNCTION auth.role() RETURNS text - LANGUAGE sql STABLE - AS $$ - select - coalesce( - nullif(current_setting('request.jwt.claim.role', true), ''), - (nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'role') - )::text -$$; - - -ALTER FUNCTION auth.role() OWNER TO supabase_auth_admin; - --- --- Name: FUNCTION role(); Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON FUNCTION auth.role() IS 'Deprecated. Use auth.jwt() -> ''role'' instead.'; - - --- --- Name: uid(); Type: FUNCTION; Schema: auth; Owner: supabase_auth_admin --- - -CREATE OR REPLACE FUNCTION auth.uid() RETURNS uuid - LANGUAGE sql STABLE - AS $$ - select - coalesce( - nullif(current_setting('request.jwt.claim.sub', true), ''), - (nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub') - )::uuid -$$; - - -ALTER FUNCTION auth.uid() OWNER TO supabase_auth_admin; - --- --- Name: FUNCTION uid(); Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON FUNCTION auth.uid() IS 'Deprecated. Use auth.jwt() -> ''sub'' instead.'; - - --- --- Name: grant_pg_cron_access(); Type: FUNCTION; Schema: extensions; Owner: postgres --- - -CREATE OR REPLACE FUNCTION extensions.grant_pg_cron_access() RETURNS event_trigger - LANGUAGE plpgsql - AS $$ -DECLARE - schema_is_cron bool; -BEGIN - schema_is_cron = ( - SELECT n.nspname = 'cron' - FROM pg_event_trigger_ddl_commands() AS ev - LEFT JOIN pg_catalog.pg_namespace AS n - ON ev.objid = n.oid - ); - - IF schema_is_cron - THEN - grant usage on schema cron to postgres with grant option; - - alter default privileges in schema cron grant all on tables to postgres with grant option; - alter default privileges in schema cron grant all on functions to postgres with grant option; - alter default privileges in schema cron grant all on sequences to postgres with grant option; - - alter default privileges for user supabase_admin in schema cron grant all - on sequences to postgres with grant option; - alter default privileges for user supabase_admin in schema cron grant all - on tables to postgres with grant option; - alter default privileges for user supabase_admin in schema cron grant all - on functions to postgres with grant option; - - grant all privileges on all tables in schema cron to postgres with grant option; - - END IF; - -END; -$$; - - -ALTER FUNCTION extensions.grant_pg_cron_access() OWNER TO postgres; - --- --- Name: FUNCTION grant_pg_cron_access(); Type: COMMENT; Schema: extensions; Owner: postgres --- - -COMMENT ON FUNCTION extensions.grant_pg_cron_access() IS 'Grants access to pg_cron'; - - --- --- Name: grant_pg_graphql_access(); Type: FUNCTION; Schema: extensions; Owner: supabase_admin --- - -CREATE OR REPLACE FUNCTION extensions.grant_pg_graphql_access() RETURNS event_trigger - LANGUAGE plpgsql - AS $_$ -DECLARE - func_is_graphql_resolve bool; -BEGIN - func_is_graphql_resolve = ( - SELECT n.proname = 'resolve' - FROM pg_event_trigger_ddl_commands() AS ev - LEFT JOIN pg_catalog.pg_proc AS n - ON ev.objid = n.oid - ); - - IF func_is_graphql_resolve - THEN - -- Update public wrapper to pass all arguments through to the pg_graphql resolve func - DROP FUNCTION IF EXISTS graphql_public.graphql; - create or replace function graphql_public.graphql( - "operationName" text default null, - query text default null, - variables jsonb default null, - extensions jsonb default null - ) - returns jsonb - language sql - as $$ - select graphql.resolve( - query := query, - variables := coalesce(variables, '{}'), - "operationName" := "operationName", - extensions := extensions - ); - $$; - - -- This hook executes when `graphql.resolve` is created. That is not necessarily the last - -- function in the extension so we need to grant permissions on existing entities AND - -- update default permissions to any others that are created after `graphql.resolve` - grant usage on schema graphql to postgres, anon, authenticated, service_role; - grant select on all tables in schema graphql to postgres, anon, authenticated, service_role; - grant execute on all functions in schema graphql to postgres, anon, authenticated, service_role; - grant all on all sequences in schema graphql to postgres, anon, authenticated, service_role; - alter default privileges in schema graphql grant all on tables to postgres, anon, authenticated, service_role; - alter default privileges in schema graphql grant all on functions to postgres, anon, authenticated, service_role; - alter default privileges in schema graphql grant all on sequences to postgres, anon, authenticated, service_role; - END IF; - -END; -$_$; - - -ALTER FUNCTION extensions.grant_pg_graphql_access() OWNER TO supabase_admin; - --- --- Name: FUNCTION grant_pg_graphql_access(); Type: COMMENT; Schema: extensions; Owner: supabase_admin --- - -COMMENT ON FUNCTION extensions.grant_pg_graphql_access() IS 'Grants access to pg_graphql'; - - --- --- Name: grant_pg_net_access(); Type: FUNCTION; Schema: extensions; Owner: postgres --- - -CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access() RETURNS event_trigger - LANGUAGE plpgsql - AS $$ -BEGIN - IF EXISTS ( - SELECT 1 - FROM pg_event_trigger_ddl_commands() AS ev - JOIN pg_extension AS ext - ON ev.objid = ext.oid - WHERE ext.extname = 'pg_net' - ) - THEN - GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role; - - ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; - ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; - - ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; - ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; - - REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; - REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; - - GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; - GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; - END IF; -END; -$$; - - -ALTER FUNCTION extensions.grant_pg_net_access() OWNER TO postgres; - --- --- Name: FUNCTION grant_pg_net_access(); Type: COMMENT; Schema: extensions; Owner: postgres --- - -COMMENT ON FUNCTION extensions.grant_pg_net_access() IS 'Grants access to pg_net'; - - --- --- Name: pgrst_ddl_watch(); Type: FUNCTION; Schema: extensions; Owner: supabase_admin --- - -CREATE OR REPLACE FUNCTION extensions.pgrst_ddl_watch() RETURNS event_trigger - LANGUAGE plpgsql - AS $$ -DECLARE - cmd record; -BEGIN - FOR cmd IN SELECT * FROM pg_event_trigger_ddl_commands() - LOOP - IF cmd.command_tag IN ( - 'CREATE SCHEMA', 'ALTER SCHEMA' - , 'CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO', 'ALTER TABLE' - , 'CREATE FOREIGN TABLE', 'ALTER FOREIGN TABLE' - , 'CREATE VIEW', 'ALTER VIEW' - , 'CREATE MATERIALIZED VIEW', 'ALTER MATERIALIZED VIEW' - , 'CREATE FUNCTION', 'ALTER FUNCTION' - , 'CREATE TRIGGER' - , 'CREATE TYPE', 'ALTER TYPE' - , 'CREATE RULE' - , 'COMMENT' - ) - -- don't notify in case of CREATE TEMP table or other objects created on pg_temp - AND cmd.schema_name is distinct from 'pg_temp' - THEN - NOTIFY pgrst, 'reload schema'; - END IF; - END LOOP; -END; $$; - - -ALTER FUNCTION extensions.pgrst_ddl_watch() OWNER TO supabase_admin; - --- --- Name: pgrst_drop_watch(); Type: FUNCTION; Schema: extensions; Owner: supabase_admin --- - -CREATE OR REPLACE FUNCTION extensions.pgrst_drop_watch() RETURNS event_trigger - LANGUAGE plpgsql - AS $$ -DECLARE - obj record; -BEGIN - FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects() - LOOP - IF obj.object_type IN ( - 'schema' - , 'table' - , 'foreign table' - , 'view' - , 'materialized view' - , 'function' - , 'trigger' - , 'type' - , 'rule' - ) - AND obj.is_temporary IS false -- no pg_temp objects - THEN - NOTIFY pgrst, 'reload schema'; - END IF; - END LOOP; -END; $$; - - -ALTER FUNCTION extensions.pgrst_drop_watch() OWNER TO supabase_admin; - --- --- Name: set_graphql_placeholder(); Type: FUNCTION; Schema: extensions; Owner: supabase_admin --- - -CREATE OR REPLACE FUNCTION extensions.set_graphql_placeholder() RETURNS event_trigger - LANGUAGE plpgsql - AS $_$ - DECLARE - graphql_is_dropped bool; - BEGIN - graphql_is_dropped = ( - SELECT ev.schema_name = 'graphql_public' - FROM pg_event_trigger_dropped_objects() AS ev - WHERE ev.schema_name = 'graphql_public' - ); - - IF graphql_is_dropped - THEN - create or replace function graphql_public.graphql( - "operationName" text default null, - query text default null, - variables jsonb default null, - extensions jsonb default null - ) - returns jsonb - language plpgsql - as $$ - DECLARE - server_version float; - BEGIN - server_version = (SELECT (SPLIT_PART((select version()), ' ', 2))::float); - - IF server_version >= 14 THEN - RETURN jsonb_build_object( - 'errors', jsonb_build_array( - jsonb_build_object( - 'message', 'pg_graphql extension is not enabled.' - ) - ) - ); - ELSE - RETURN jsonb_build_object( - 'errors', jsonb_build_array( - jsonb_build_object( - 'message', 'pg_graphql is only available on projects running Postgres 14 onwards.' - ) - ) - ); - END IF; - END; - $$; - END IF; - - END; -$_$; - - -ALTER FUNCTION extensions.set_graphql_placeholder() OWNER TO supabase_admin; - --- --- Name: FUNCTION set_graphql_placeholder(); Type: COMMENT; Schema: extensions; Owner: supabase_admin --- - -COMMENT ON FUNCTION extensions.set_graphql_placeholder() IS 'Reintroduces placeholder function for graphql_public.graphql'; - - --- --- Name: get_auth(text); Type: FUNCTION; Schema: pgbouncer; Owner: postgres --- - -CREATE OR REPLACE FUNCTION pgbouncer.get_auth(p_usename text) RETURNS TABLE(username text, password text) - LANGUAGE plpgsql SECURITY DEFINER - AS $$ -BEGIN - RAISE WARNING 'PgBouncer auth request: %', p_usename; - - RETURN QUERY - SELECT usename::TEXT, passwd::TEXT FROM pg_catalog.pg_shadow - WHERE usename = p_usename; -END; -$$; - - -ALTER FUNCTION pgbouncer.get_auth(p_usename text) OWNER TO postgres; - --- --- Name: can_insert_object(text, text, uuid, jsonb); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin --- - -CREATE OR REPLACE FUNCTION storage.can_insert_object(bucketid text, name text, owner uuid, metadata jsonb) RETURNS void - LANGUAGE plpgsql - AS $$ -BEGIN - INSERT INTO "storage"."objects" ("bucket_id", "name", "owner", "metadata") VALUES (bucketid, name, owner, metadata); - -- hack to rollback the successful insert - RAISE sqlstate 'PT200' using - message = 'ROLLBACK', - detail = 'rollback successful insert'; -END -$$; - - -ALTER FUNCTION storage.can_insert_object(bucketid text, name text, owner uuid, metadata jsonb) OWNER TO supabase_storage_admin; - --- --- Name: extension(text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin --- - -CREATE OR REPLACE FUNCTION storage.extension(name text) RETURNS text - LANGUAGE plpgsql - AS $$ -DECLARE -_parts text[]; -_filename text; -BEGIN - select string_to_array(name, '/') into _parts; - select _parts[array_length(_parts,1)] into _filename; - -- @todo return the last part instead of 2 - return split_part(_filename, '.', 2); -END -$$; - - -ALTER FUNCTION storage.extension(name text) OWNER TO supabase_storage_admin; - --- --- Name: filename(text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin --- - -CREATE OR REPLACE FUNCTION storage.filename(name text) RETURNS text - LANGUAGE plpgsql - AS $$ -DECLARE -_parts text[]; -BEGIN - select string_to_array(name, '/') into _parts; - return _parts[array_length(_parts,1)]; -END -$$; - - -ALTER FUNCTION storage.filename(name text) OWNER TO supabase_storage_admin; - --- --- Name: foldername(text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin --- - -CREATE OR REPLACE FUNCTION storage.foldername(name text) RETURNS text[] - LANGUAGE plpgsql - AS $$ -DECLARE -_parts text[]; -BEGIN - select string_to_array(name, '/') into _parts; - return _parts[1:array_length(_parts,1)-1]; -END -$$; - - -ALTER FUNCTION storage.foldername(name text) OWNER TO supabase_storage_admin; - --- --- Name: get_size_by_bucket(); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin --- - -CREATE OR REPLACE FUNCTION storage.get_size_by_bucket() RETURNS TABLE(size bigint, bucket_id text) - LANGUAGE plpgsql - AS $$ -BEGIN - return query - select sum((metadata->>'size')::int) as size, obj.bucket_id - from "storage".objects as obj - group by obj.bucket_id; -END -$$; - - -ALTER FUNCTION storage.get_size_by_bucket() OWNER TO supabase_storage_admin; - --- --- Name: search(text, text, integer, integer, integer, text, text, text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin --- - -CREATE OR REPLACE FUNCTION storage.search(prefix text, bucketname text, limits integer DEFAULT 100, levels integer DEFAULT 1, offsets integer DEFAULT 0, search text DEFAULT ''::text, sortcolumn text DEFAULT 'name'::text, sortorder text DEFAULT 'asc'::text) RETURNS TABLE(name text, id uuid, updated_at timestamp with time zone, created_at timestamp with time zone, last_accessed_at timestamp with time zone, metadata jsonb) - LANGUAGE plpgsql STABLE - AS $_$ -declare - v_order_by text; - v_sort_order text; -begin - case - when sortcolumn = 'name' then - v_order_by = 'name'; - when sortcolumn = 'updated_at' then - v_order_by = 'updated_at'; - when sortcolumn = 'created_at' then - v_order_by = 'created_at'; - when sortcolumn = 'last_accessed_at' then - v_order_by = 'last_accessed_at'; - else - v_order_by = 'name'; - end case; - - case - when sortorder = 'asc' then - v_sort_order = 'asc'; - when sortorder = 'desc' then - v_sort_order = 'desc'; - else - v_sort_order = 'asc'; - end case; - - v_order_by = v_order_by || ' ' || v_sort_order; - - return query execute - 'with folders as ( - select path_tokens[$1] as folder - from storage.objects - where objects.name ilike $2 || $3 || ''%'' - and bucket_id = $4 - and array_length(regexp_split_to_array(objects.name, ''/''), 1) <> $1 - group by folder - order by folder ' || v_sort_order || ' - ) - (select folder as "name", - null as id, - null as updated_at, - null as created_at, - null as last_accessed_at, - null as metadata from folders) - union all - (select path_tokens[$1] as "name", - id, - updated_at, - created_at, - last_accessed_at, - metadata - from storage.objects - where objects.name ilike $2 || $3 || ''%'' - and bucket_id = $4 - and array_length(regexp_split_to_array(objects.name, ''/''), 1) = $1 - order by ' || v_order_by || ') - limit $5 - offset $6' using levels, prefix, search, bucketname, limits, offsets; -end; -$_$; - - -ALTER FUNCTION storage.search(prefix text, bucketname text, limits integer, levels integer, offsets integer, search text, sortcolumn text, sortorder text) OWNER TO supabase_storage_admin; - --- --- Name: update_updated_at_column(); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin --- - -CREATE OR REPLACE FUNCTION storage.update_updated_at_column() RETURNS trigger - LANGUAGE plpgsql - AS $$ -BEGIN - NEW.updated_at = now(); - RETURN NEW; -END; -$$; - - -ALTER FUNCTION storage.update_updated_at_column() OWNER TO supabase_storage_admin; - --- --- Name: http_request(); Type: FUNCTION; Schema: supabase_functions; Owner: supabase_functions_admin --- - -CREATE OR REPLACE FUNCTION supabase_functions.http_request() RETURNS trigger - LANGUAGE plpgsql SECURITY DEFINER - SET search_path TO 'supabase_functions' - AS $$ - DECLARE - request_id bigint; - payload jsonb; - url text := TG_ARGV[0]::text; - method text := TG_ARGV[1]::text; - headers jsonb DEFAULT '{}'::jsonb; - params jsonb DEFAULT '{}'::jsonb; - timeout_ms integer DEFAULT 1000; - BEGIN - IF url IS NULL OR url = 'null' THEN - RAISE EXCEPTION 'url argument is missing'; - END IF; - - IF method IS NULL OR method = 'null' THEN - RAISE EXCEPTION 'method argument is missing'; - END IF; - - IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN - headers = '{"Content-Type": "application/json"}'::jsonb; - ELSE - headers = TG_ARGV[2]::jsonb; - END IF; - - IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN - params = '{}'::jsonb; - ELSE - params = TG_ARGV[3]::jsonb; - END IF; - - IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN - timeout_ms = 1000; - ELSE - timeout_ms = TG_ARGV[4]::integer; - END IF; - - CASE - WHEN method = 'GET' THEN - SELECT http_get INTO request_id FROM net.http_get( - url, - params, - headers, - timeout_ms - ); - WHEN method = 'POST' THEN - payload = jsonb_build_object( - 'old_record', OLD, - 'record', NEW, - 'type', TG_OP, - 'table', TG_TABLE_NAME, - 'schema', TG_TABLE_SCHEMA - ); - - SELECT http_post INTO request_id FROM net.http_post( - url, - payload, - params, - headers, - timeout_ms - ); - ELSE - RAISE EXCEPTION 'method argument % is invalid', method; - END CASE; - - INSERT INTO supabase_functions.hooks - (hook_table_id, hook_name, request_id) - VALUES - (TG_RELID, TG_NAME, request_id); - - RETURN NEW; - END -$$; - - -ALTER FUNCTION supabase_functions.http_request() OWNER TO supabase_functions_admin; - -SET default_tablespace = ''; - -SET default_table_access_method = heap; - --- --- Name: extensions; Type: TABLE; Schema: _realtime; Owner: postgres --- - -CREATE TABLE IF NOT EXISTS _realtime.extensions ( - id uuid NOT NULL, - type text, - settings jsonb, - tenant_external_id text, - inserted_at timestamp(0) without time zone NOT NULL, - updated_at timestamp(0) without time zone NOT NULL -); - - -ALTER TABLE _realtime.extensions OWNER TO postgres; - --- --- Name: schema_migrations; Type: TABLE; Schema: _realtime; Owner: postgres --- - -CREATE TABLE IF NOT EXISTS _realtime.schema_migrations ( - version bigint NOT NULL, - inserted_at timestamp(0) without time zone -); - - -ALTER TABLE _realtime.schema_migrations OWNER TO postgres; - --- --- Name: tenants; Type: TABLE; Schema: _realtime; Owner: postgres --- - -CREATE TABLE IF NOT EXISTS _realtime.tenants ( - id uuid NOT NULL, - name text, - external_id text, - jwt_secret text, - max_concurrent_users integer DEFAULT 200 NOT NULL, - inserted_at timestamp(0) without time zone NOT NULL, - updated_at timestamp(0) without time zone NOT NULL, - max_events_per_second integer DEFAULT 100 NOT NULL, - postgres_cdc_default text DEFAULT 'postgres_cdc_rls'::text, - max_bytes_per_second integer DEFAULT 100000 NOT NULL, - max_channels_per_client integer DEFAULT 100 NOT NULL, - max_joins_per_second integer DEFAULT 500 NOT NULL, - suspend boolean DEFAULT false -); - - -ALTER TABLE _realtime.tenants OWNER TO postgres; - --- --- Name: audit_log_entries; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.audit_log_entries ( - instance_id uuid, - id uuid NOT NULL, - payload json, - created_at timestamp with time zone, - ip_address character varying(64) DEFAULT ''::character varying NOT NULL -); - - -ALTER TABLE auth.audit_log_entries OWNER TO supabase_auth_admin; - --- --- Name: TABLE audit_log_entries; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.audit_log_entries IS 'Auth: Audit trail for user actions.'; - - --- --- Name: flow_state; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.flow_state ( - id uuid NOT NULL, - user_id uuid, - auth_code text NOT NULL, - code_challenge_method auth.code_challenge_method NOT NULL, - code_challenge text NOT NULL, - provider_type text NOT NULL, - provider_access_token text, - provider_refresh_token text, - created_at timestamp with time zone, - updated_at timestamp with time zone, - authentication_method text NOT NULL -); - - -ALTER TABLE auth.flow_state OWNER TO supabase_auth_admin; - --- --- Name: TABLE flow_state; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.flow_state IS 'stores metadata for pkce logins'; - - --- --- Name: identities; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.identities ( - provider_id text NOT NULL, - user_id uuid NOT NULL, - identity_data jsonb NOT NULL, - provider text NOT NULL, - last_sign_in_at timestamp with time zone, - created_at timestamp with time zone, - updated_at timestamp with time zone, - email text GENERATED ALWAYS AS (lower((identity_data ->> 'email'::text))) STORED, - id uuid DEFAULT gen_random_uuid() NOT NULL -); - - -ALTER TABLE auth.identities OWNER TO supabase_auth_admin; - --- --- Name: TABLE identities; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.identities IS 'Auth: Stores identities associated to a user.'; - - --- --- Name: COLUMN identities.email; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON COLUMN auth.identities.email IS 'Auth: Email is a generated column that references the optional email property in the identity_data'; - - --- --- Name: instances; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.instances ( - id uuid NOT NULL, - uuid uuid, - raw_base_config text, - created_at timestamp with time zone, - updated_at timestamp with time zone -); - - -ALTER TABLE auth.instances OWNER TO supabase_auth_admin; - --- --- Name: TABLE instances; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.instances IS 'Auth: Manages users across multiple sites.'; - - --- --- Name: mfa_amr_claims; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.mfa_amr_claims ( - session_id uuid NOT NULL, - created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL, - authentication_method text NOT NULL, - id uuid NOT NULL -); - - -ALTER TABLE auth.mfa_amr_claims OWNER TO supabase_auth_admin; - --- --- Name: TABLE mfa_amr_claims; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.mfa_amr_claims IS 'auth: stores authenticator method reference claims for multi factor authentication'; - - --- --- Name: mfa_challenges; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.mfa_challenges ( - id uuid NOT NULL, - factor_id uuid NOT NULL, - created_at timestamp with time zone NOT NULL, - verified_at timestamp with time zone, - ip_address inet NOT NULL -); - - -ALTER TABLE auth.mfa_challenges OWNER TO supabase_auth_admin; - --- --- Name: TABLE mfa_challenges; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.mfa_challenges IS 'auth: stores metadata about challenge requests made'; - - --- --- Name: mfa_factors; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.mfa_factors ( - id uuid NOT NULL, - user_id uuid NOT NULL, - friendly_name text, - factor_type auth.factor_type NOT NULL, - status auth.factor_status NOT NULL, - created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL, - secret text -); - - -ALTER TABLE auth.mfa_factors OWNER TO supabase_auth_admin; - --- --- Name: TABLE mfa_factors; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.mfa_factors IS 'auth: stores metadata about factors'; - - --- --- Name: refresh_tokens; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.refresh_tokens ( - instance_id uuid, - id bigint NOT NULL, - token character varying(255), - user_id character varying(255), - revoked boolean, - created_at timestamp with time zone, - updated_at timestamp with time zone, - parent character varying(255), - session_id uuid -); - - -ALTER TABLE auth.refresh_tokens OWNER TO supabase_auth_admin; - --- --- Name: TABLE refresh_tokens; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.refresh_tokens IS 'Auth: Store of tokens used to refresh JWT tokens once they expire.'; - - --- --- Name: refresh_tokens_id_seq; Type: SEQUENCE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE SEQUENCE IF NOT EXISTS auth.refresh_tokens_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - -ALTER TABLE auth.refresh_tokens_id_seq OWNER TO supabase_auth_admin; - --- --- Name: refresh_tokens_id_seq; Type: SEQUENCE OWNED BY; Schema: auth; Owner: supabase_auth_admin --- - -ALTER SEQUENCE auth.refresh_tokens_id_seq OWNED BY auth.refresh_tokens.id; - - --- --- Name: saml_providers; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.saml_providers ( - id uuid NOT NULL, - sso_provider_id uuid NOT NULL, - entity_id text NOT NULL, - metadata_xml text NOT NULL, - metadata_url text, - attribute_mapping jsonb, - created_at timestamp with time zone, - updated_at timestamp with time zone, - CONSTRAINT "entity_id not empty" CHECK ((char_length(entity_id) > 0)), - CONSTRAINT "metadata_url not empty" CHECK (((metadata_url = NULL::text) OR (char_length(metadata_url) > 0))), - CONSTRAINT "metadata_xml not empty" CHECK ((char_length(metadata_xml) > 0)) -); - - -ALTER TABLE auth.saml_providers OWNER TO supabase_auth_admin; - --- --- Name: TABLE saml_providers; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.saml_providers IS 'Auth: Manages SAML Identity Provider connections.'; - - --- --- Name: saml_relay_states; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.saml_relay_states ( - id uuid NOT NULL, - sso_provider_id uuid NOT NULL, - request_id text NOT NULL, - for_email text, - redirect_to text, - from_ip_address inet, - created_at timestamp with time zone, - updated_at timestamp with time zone, - flow_state_id uuid, - CONSTRAINT "request_id not empty" CHECK ((char_length(request_id) > 0)) -); - - -ALTER TABLE auth.saml_relay_states OWNER TO supabase_auth_admin; - --- --- Name: TABLE saml_relay_states; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.saml_relay_states IS 'Auth: Contains SAML Relay State information for each Service Provider initiated login.'; - - --- --- Name: schema_migrations; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.schema_migrations ( - version character varying(255) NOT NULL -); - - -ALTER TABLE auth.schema_migrations OWNER TO supabase_auth_admin; - --- --- Name: TABLE schema_migrations; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.schema_migrations IS 'Auth: Manages updates to the auth system.'; - - --- --- Name: sessions; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.sessions ( - id uuid NOT NULL, - user_id uuid NOT NULL, - created_at timestamp with time zone, - updated_at timestamp with time zone, - factor_id uuid, - aal auth.aal_level, - not_after timestamp with time zone, - refreshed_at timestamp without time zone, - user_agent text, - ip inet, - tag text -); - - -ALTER TABLE auth.sessions OWNER TO supabase_auth_admin; - --- --- Name: TABLE sessions; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.sessions IS 'Auth: Stores session data associated to a user.'; - - --- --- Name: COLUMN sessions.not_after; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON COLUMN auth.sessions.not_after IS 'Auth: Not after is a nullable column that contains a timestamp after which the session should be regarded as expired.'; - - --- --- Name: sso_domains; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.sso_domains ( - id uuid NOT NULL, - sso_provider_id uuid NOT NULL, - domain text NOT NULL, - created_at timestamp with time zone, - updated_at timestamp with time zone, - CONSTRAINT "domain not empty" CHECK ((char_length(domain) > 0)) -); - - -ALTER TABLE auth.sso_domains OWNER TO supabase_auth_admin; - --- --- Name: TABLE sso_domains; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.sso_domains IS 'Auth: Manages SSO email address domain mapping to an SSO Identity Provider.'; - - --- --- Name: sso_providers; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.sso_providers ( - id uuid NOT NULL, - resource_id text, - created_at timestamp with time zone, - updated_at timestamp with time zone, - CONSTRAINT "resource_id not empty" CHECK (((resource_id = NULL::text) OR (char_length(resource_id) > 0))) -); - - -ALTER TABLE auth.sso_providers OWNER TO supabase_auth_admin; - --- --- Name: TABLE sso_providers; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.sso_providers IS 'Auth: Manages SSO identity provider information; see saml_providers for SAML.'; - - --- --- Name: COLUMN sso_providers.resource_id; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON COLUMN auth.sso_providers.resource_id IS 'Auth: Uniquely identifies a SSO provider according to a user-chosen resource ID (case insensitive), useful in infrastructure as code.'; - - --- --- Name: users; Type: TABLE; Schema: auth; Owner: supabase_auth_admin --- - -CREATE TABLE IF NOT EXISTS auth.users ( - instance_id uuid, - id uuid NOT NULL, - aud character varying(255), - role character varying(255), - email character varying(255), - encrypted_password character varying(255), - email_confirmed_at timestamp with time zone, - invited_at timestamp with time zone, - confirmation_token character varying(255), - confirmation_sent_at timestamp with time zone, - recovery_token character varying(255), - recovery_sent_at timestamp with time zone, - email_change_token_new character varying(255), - email_change character varying(255), - email_change_sent_at timestamp with time zone, - last_sign_in_at timestamp with time zone, - raw_app_meta_data jsonb, - raw_user_meta_data jsonb, - is_super_admin boolean, - created_at timestamp with time zone, - updated_at timestamp with time zone, - phone text DEFAULT NULL::character varying, - phone_confirmed_at timestamp with time zone, - phone_change text DEFAULT ''::character varying, - phone_change_token character varying(255) DEFAULT ''::character varying, - phone_change_sent_at timestamp with time zone, - confirmed_at timestamp with time zone GENERATED ALWAYS AS (LEAST(email_confirmed_at, phone_confirmed_at)) STORED, - email_change_token_current character varying(255) DEFAULT ''::character varying, - email_change_confirm_status smallint DEFAULT 0, - banned_until timestamp with time zone, - reauthentication_token character varying(255) DEFAULT ''::character varying, - reauthentication_sent_at timestamp with time zone, - is_sso_user boolean DEFAULT false NOT NULL, - deleted_at timestamp with time zone, - CONSTRAINT users_email_change_confirm_status_check CHECK (((email_change_confirm_status >= 0) AND (email_change_confirm_status <= 2))) -); - - -ALTER TABLE auth.users OWNER TO supabase_auth_admin; - --- --- Name: TABLE users; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON TABLE auth.users IS 'Auth: Stores user login data within a secure schema.'; - - --- --- Name: COLUMN users.is_sso_user; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON COLUMN auth.users.is_sso_user IS 'Auth: Set this column to true when the account comes from SSO. These accounts can have duplicate emails.'; - - --- --- Name: buckets; Type: TABLE; Schema: storage; Owner: supabase_storage_admin --- - -CREATE TABLE IF NOT EXISTS storage.buckets ( - id text NOT NULL, - name text NOT NULL, - owner uuid, - created_at timestamp with time zone DEFAULT now(), - updated_at timestamp with time zone DEFAULT now(), - public boolean DEFAULT false, - avif_autodetection boolean DEFAULT false, - file_size_limit bigint, - allowed_mime_types text[], - owner_id text -); - - -ALTER TABLE storage.buckets OWNER TO supabase_storage_admin; - --- --- Name: COLUMN buckets.owner; Type: COMMENT; Schema: storage; Owner: supabase_storage_admin --- - -COMMENT ON COLUMN storage.buckets.owner IS 'Field is deprecated, use owner_id instead'; - - --- --- Name: migrations; Type: TABLE; Schema: storage; Owner: supabase_storage_admin --- - -CREATE TABLE IF NOT EXISTS storage.migrations ( - id integer NOT NULL, - name character varying(100) NOT NULL, - hash character varying(40) NOT NULL, - executed_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP -); - - -ALTER TABLE storage.migrations OWNER TO supabase_storage_admin; - --- --- Name: objects; Type: TABLE; Schema: storage; Owner: supabase_storage_admin --- - -CREATE TABLE IF NOT EXISTS storage.objects ( - id uuid DEFAULT extensions.uuid_generate_v4() NOT NULL, - bucket_id text, - name text, - owner uuid, - created_at timestamp with time zone DEFAULT now(), - updated_at timestamp with time zone DEFAULT now(), - last_accessed_at timestamp with time zone DEFAULT now(), - metadata jsonb, - path_tokens text[] GENERATED ALWAYS AS (string_to_array(name, '/'::text)) STORED, - version text, - owner_id text -); - - -ALTER TABLE storage.objects OWNER TO supabase_storage_admin; - --- --- Name: COLUMN objects.owner; Type: COMMENT; Schema: storage; Owner: supabase_storage_admin --- - -COMMENT ON COLUMN storage.objects.owner IS 'Field is deprecated, use owner_id instead'; - - --- --- Name: hooks; Type: TABLE; Schema: supabase_functions; Owner: supabase_functions_admin --- - -CREATE TABLE IF NOT EXISTS supabase_functions.hooks ( - id bigint NOT NULL, - hook_table_id integer NOT NULL, - hook_name text NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL, - request_id bigint -); - - -ALTER TABLE supabase_functions.hooks OWNER TO supabase_functions_admin; - --- --- Name: TABLE hooks; Type: COMMENT; Schema: supabase_functions; Owner: supabase_functions_admin --- - -COMMENT ON TABLE supabase_functions.hooks IS 'Supabase Functions Hooks: Audit trail for triggered hooks.'; - - --- --- Name: hooks_id_seq; Type: SEQUENCE; Schema: supabase_functions; Owner: supabase_functions_admin --- - -CREATE SEQUENCE IF NOT EXISTS supabase_functions.hooks_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - -ALTER TABLE supabase_functions.hooks_id_seq OWNER TO supabase_functions_admin; - --- --- Name: hooks_id_seq; Type: SEQUENCE OWNED BY; Schema: supabase_functions; Owner: supabase_functions_admin --- - -ALTER SEQUENCE supabase_functions.hooks_id_seq OWNED BY supabase_functions.hooks.id; - - --- --- Name: migrations; Type: TABLE; Schema: supabase_functions; Owner: supabase_functions_admin --- - -CREATE TABLE IF NOT EXISTS supabase_functions.migrations ( - version text NOT NULL, - inserted_at timestamp with time zone DEFAULT now() NOT NULL -); - - -ALTER TABLE supabase_functions.migrations OWNER TO supabase_functions_admin; - --- --- Name: refresh_tokens id; Type: DEFAULT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.refresh_tokens ALTER COLUMN id SET DEFAULT nextval('auth.refresh_tokens_id_seq'::regclass); - - --- --- Name: hooks id; Type: DEFAULT; Schema: supabase_functions; Owner: supabase_functions_admin --- - -ALTER TABLE ONLY supabase_functions.hooks ALTER COLUMN id SET DEFAULT nextval('supabase_functions.hooks_id_seq'::regclass); - - --- --- Data for Name: extensions; Type: TABLE DATA; Schema: _realtime; Owner: postgres --- - -INSERT INTO _realtime.extensions (id, type, settings, tenant_external_id, inserted_at, updated_at) VALUES ('832b3aeb-9d7f-41aa-ad31-60ea054c294f', 'postgres_cdc_rls', '{"region": "us-east-1", "db_host": "ABK7kBu27y/PVdL10i/b+A==", "db_name": "sWBpZNdjggEPTQVlI52Zfw==", "db_port": "+enMDFi1J/3IrrquHHwUmA==", "db_user": "uxbEq/zz8DXVD53TOI1zmw==", "slot_name": "supabase_realtime_replication_slot", "ip_version": 4, "db_password": "sWBpZNdjggEPTQVlI52Zfw==", "publication": "supabase_realtime", "ssl_enforced": false, "poll_interval_ms": 100, "poll_max_changes": 100, "poll_max_record_bytes": 1048576}', 'realtime-dev', '2023-11-20 22:04:38', '2023-11-20 22:04:38'); - - --- --- Data for Name: schema_migrations; Type: TABLE DATA; Schema: _realtime; Owner: postgres --- - -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20210706140551, '2023-03-16 02:25:35'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20220329161857, '2023-03-16 02:25:35'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20220410212326, '2023-03-16 02:25:35'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20220506102948, '2023-03-16 02:25:35'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20220527210857, '2023-03-16 02:25:36'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20220815211129, '2023-03-16 02:25:36'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20220815215024, '2023-03-16 02:25:36'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20220818141501, '2023-03-16 02:25:36'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20221018173709, '2023-03-16 02:25:36'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20221102172703, '2023-03-16 02:25:36'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20221223010058, '2023-03-16 02:25:36'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20230110180046, '2023-03-16 02:25:36'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20230810220907, '2023-11-20 21:54:23'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20230810220924, '2023-11-20 21:54:23'); -INSERT INTO _realtime.schema_migrations (version, inserted_at) VALUES (20231024094642, '2023-11-20 21:54:23'); - - --- --- Data for Name: tenants; Type: TABLE DATA; Schema: _realtime; Owner: postgres --- - -INSERT INTO _realtime.tenants (id, name, external_id, jwt_secret, max_concurrent_users, inserted_at, updated_at, max_events_per_second, postgres_cdc_default, max_bytes_per_second, max_channels_per_client, max_joins_per_second, suspend) VALUES ('f418662c-07b5-4160-9338-4db7fbaaabbd', 'realtime-dev', 'realtime-dev', 'iNjicxc4+llvc9wovDvqymwfnj9teWMlyOIbJ8Fh6j2WNU8CIJ2ZgjR6MUIKqSmeDmvpsKLsZ9jgXJmQPpwL8w==', 200, '2023-11-20 22:04:38', '2023-11-20 22:04:38', 100, 'postgres_cdc_rls', 100000, 100, 100, false); - - --- --- Data for Name: audit_log_entries; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: flow_state; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: identities; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: instances; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: mfa_amr_claims; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: mfa_challenges; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: mfa_factors; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: refresh_tokens; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: saml_providers; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: saml_relay_states; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: schema_migrations; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - -INSERT INTO auth.schema_migrations (version) VALUES ('20171026211738'); -INSERT INTO auth.schema_migrations (version) VALUES ('20171026211808'); -INSERT INTO auth.schema_migrations (version) VALUES ('20171026211834'); -INSERT INTO auth.schema_migrations (version) VALUES ('20180103212743'); -INSERT INTO auth.schema_migrations (version) VALUES ('20180108183307'); -INSERT INTO auth.schema_migrations (version) VALUES ('20180119214651'); -INSERT INTO auth.schema_migrations (version) VALUES ('20180125194653'); -INSERT INTO auth.schema_migrations (version) VALUES ('00'); -INSERT INTO auth.schema_migrations (version) VALUES ('20210710035447'); -INSERT INTO auth.schema_migrations (version) VALUES ('20210722035447'); -INSERT INTO auth.schema_migrations (version) VALUES ('20210730183235'); -INSERT INTO auth.schema_migrations (version) VALUES ('20210909172000'); -INSERT INTO auth.schema_migrations (version) VALUES ('20210927181326'); -INSERT INTO auth.schema_migrations (version) VALUES ('20211122151130'); -INSERT INTO auth.schema_migrations (version) VALUES ('20211124214934'); -INSERT INTO auth.schema_migrations (version) VALUES ('20211202183645'); -INSERT INTO auth.schema_migrations (version) VALUES ('20220114185221'); -INSERT INTO auth.schema_migrations (version) VALUES ('20220114185340'); -INSERT INTO auth.schema_migrations (version) VALUES ('20220224000811'); -INSERT INTO auth.schema_migrations (version) VALUES ('20220323170000'); -INSERT INTO auth.schema_migrations (version) VALUES ('20220429102000'); -INSERT INTO auth.schema_migrations (version) VALUES ('20220531120530'); -INSERT INTO auth.schema_migrations (version) VALUES ('20220614074223'); -INSERT INTO auth.schema_migrations (version) VALUES ('20220811173540'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221003041349'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221003041400'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221011041400'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221020193600'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221021073300'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221021082433'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221027105023'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221114143122'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221114143410'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221125140132'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221208132122'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221215195500'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221215195800'); -INSERT INTO auth.schema_migrations (version) VALUES ('20221215195900'); -INSERT INTO auth.schema_migrations (version) VALUES ('20230116124310'); -INSERT INTO auth.schema_migrations (version) VALUES ('20230116124412'); -INSERT INTO auth.schema_migrations (version) VALUES ('20230131181311'); -INSERT INTO auth.schema_migrations (version) VALUES ('20230322519590'); -INSERT INTO auth.schema_migrations (version) VALUES ('20230402418590'); -INSERT INTO auth.schema_migrations (version) VALUES ('20230411005111'); -INSERT INTO auth.schema_migrations (version) VALUES ('20230508135423'); -INSERT INTO auth.schema_migrations (version) VALUES ('20230523124323'); -INSERT INTO auth.schema_migrations (version) VALUES ('20230818113222'); -INSERT INTO auth.schema_migrations (version) VALUES ('20230914180801'); -INSERT INTO auth.schema_migrations (version) VALUES ('20231027141322'); -INSERT INTO auth.schema_migrations (version) VALUES ('20231114161723'); -INSERT INTO auth.schema_migrations (version) VALUES ('20231117164230'); - - --- --- Data for Name: sessions; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: sso_domains; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: sso_providers; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: users; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin --- - - - --- --- Data for Name: buckets; Type: TABLE DATA; Schema: storage; Owner: supabase_storage_admin --- - - - --- --- Data for Name: migrations; Type: TABLE DATA; Schema: storage; Owner: supabase_storage_admin --- - -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (0, 'create-migrations-table', 'e18db593bcde2aca2a408c4d1100f6abba2195df', '2023-03-16 02:25:37.548482'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (1, 'initialmigration', '6ab16121fbaa08bbd11b712d05f358f9b555d777', '2023-03-16 02:25:37.55595'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (2, 'pathtoken-column', '49756be03be4c17bb85fe70d4a861f27de7e49ad', '2023-03-16 02:25:37.560258'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (3, 'add-migrations-rls', 'bb5d124c53d68635a883e399426c6a5a25fc893d', '2023-03-16 02:25:37.594153'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (4, 'add-size-functions', '6d79007d04f5acd288c9c250c42d2d5fd286c54d', '2023-03-16 02:25:37.599194'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (5, 'change-column-name-in-get-size', 'fd65688505d2ffa9fbdc58a944348dd8604d688c', '2023-03-16 02:25:37.604658'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (6, 'add-rls-to-buckets', '63e2bab75a2040fee8e3fb3f15a0d26f3380e9b6', '2023-03-16 02:25:37.609563'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (7, 'add-public-to-buckets', '82568934f8a4d9e0a85f126f6fb483ad8214c418', '2023-03-16 02:25:37.615929'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (8, 'fix-search-function', '1a43a40eddb525f2e2f26efd709e6c06e58e059c', '2023-03-16 02:25:37.620171'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (9, 'search-files-search-function', '34c096597eb8b9d077fdfdde9878c88501b2fafc', '2023-03-16 02:25:37.624484'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (10, 'add-trigger-to-auto-update-updated_at-column', '37d6bb964a70a822e6d37f22f457b9bca7885928', '2023-03-16 02:25:37.629534'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (11, 'add-automatic-avif-detection-flag', 'bd76c53a9c564c80d98d119c1b3a28e16c8152db', '2023-03-16 02:25:37.634022'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (12, 'add-bucket-custom-limits', 'cbe0a4c32a0e891554a21020433b7a4423c07ee7', '2023-03-16 02:25:37.639132'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (13, 'use-bytes-for-max-size', '7a158ebce8a0c2801c9c65b7e9b2f98f68b3874e', '2023-03-16 02:25:37.643598'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (14, 'add-can-insert-object-function', '273193826bca7e0990b458d1ba72f8aa27c0d825', '2023-04-06 09:18:55.232202'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (15, 'add-version', 'e821a779d26612899b8c2dfe20245f904a327c4f', '2023-04-06 09:18:55.239444'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (16, 'drop-owner-foreign-key', '536b33f8878eed09d0144219777dcac96bdb25da', '2023-11-20 21:54:33.540386'); -INSERT INTO storage.migrations (id, name, hash, executed_at) VALUES (17, 'add_owner_id_column_deprecate_owner', '7545f216a39358b5487df75d941d05dbcd75eb46', '2023-11-20 22:04:38.959686'); - - --- --- Data for Name: objects; Type: TABLE DATA; Schema: storage; Owner: supabase_storage_admin --- - - - --- --- Data for Name: hooks; Type: TABLE DATA; Schema: supabase_functions; Owner: supabase_functions_admin --- - - - --- --- Data for Name: migrations; Type: TABLE DATA; Schema: supabase_functions; Owner: supabase_functions_admin --- - -INSERT INTO supabase_functions.migrations (version, inserted_at) VALUES ('initial', '2023-03-16 02:25:30.966697+00'); -INSERT INTO supabase_functions.migrations (version, inserted_at) VALUES ('20210809183423_update_grants', '2023-03-16 02:25:30.966697+00'); - - --- --- Name: refresh_tokens_id_seq; Type: SEQUENCE SET; Schema: auth; Owner: supabase_auth_admin --- - -SELECT pg_catalog.setval('auth.refresh_tokens_id_seq', 1, false); - - --- --- Name: hooks_id_seq; Type: SEQUENCE SET; Schema: supabase_functions; Owner: supabase_functions_admin --- - -SELECT pg_catalog.setval('supabase_functions.hooks_id_seq', 1, false); - - --- --- Name: extensions extensions_pkey; Type: CONSTRAINT; Schema: _realtime; Owner: postgres --- - -ALTER TABLE ONLY _realtime.extensions - ADD CONSTRAINT extensions_pkey PRIMARY KEY (id); - - --- --- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: _realtime; Owner: postgres --- - -ALTER TABLE ONLY _realtime.schema_migrations - ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); - - --- --- Name: tenants tenants_pkey; Type: CONSTRAINT; Schema: _realtime; Owner: postgres --- - -ALTER TABLE ONLY _realtime.tenants - ADD CONSTRAINT tenants_pkey PRIMARY KEY (id); - - --- --- Name: mfa_amr_claims amr_id_pk; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.mfa_amr_claims - ADD CONSTRAINT amr_id_pk PRIMARY KEY (id); - - --- --- Name: audit_log_entries audit_log_entries_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.audit_log_entries - ADD CONSTRAINT audit_log_entries_pkey PRIMARY KEY (id); - - --- --- Name: flow_state flow_state_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.flow_state - ADD CONSTRAINT flow_state_pkey PRIMARY KEY (id); - - --- --- Name: identities identities_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.identities - ADD CONSTRAINT identities_pkey PRIMARY KEY (id); - - --- --- Name: identities identities_provider_id_provider_unique; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.identities - ADD CONSTRAINT identities_provider_id_provider_unique UNIQUE (provider_id, provider); - - --- --- Name: instances instances_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.instances - ADD CONSTRAINT instances_pkey PRIMARY KEY (id); - - --- --- Name: mfa_amr_claims mfa_amr_claims_session_id_authentication_method_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.mfa_amr_claims - ADD CONSTRAINT mfa_amr_claims_session_id_authentication_method_pkey UNIQUE (session_id, authentication_method); - - --- --- Name: mfa_challenges mfa_challenges_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.mfa_challenges - ADD CONSTRAINT mfa_challenges_pkey PRIMARY KEY (id); - - --- --- Name: mfa_factors mfa_factors_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.mfa_factors - ADD CONSTRAINT mfa_factors_pkey PRIMARY KEY (id); - - --- --- Name: refresh_tokens refresh_tokens_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.refresh_tokens - ADD CONSTRAINT refresh_tokens_pkey PRIMARY KEY (id); - - --- --- Name: refresh_tokens refresh_tokens_token_unique; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.refresh_tokens - ADD CONSTRAINT refresh_tokens_token_unique UNIQUE (token); - - --- --- Name: saml_providers saml_providers_entity_id_key; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.saml_providers - ADD CONSTRAINT saml_providers_entity_id_key UNIQUE (entity_id); - - --- --- Name: saml_providers saml_providers_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.saml_providers - ADD CONSTRAINT saml_providers_pkey PRIMARY KEY (id); - - --- --- Name: saml_relay_states saml_relay_states_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.saml_relay_states - ADD CONSTRAINT saml_relay_states_pkey PRIMARY KEY (id); - - --- --- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.schema_migrations - ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); - - --- --- Name: sessions sessions_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.sessions - ADD CONSTRAINT sessions_pkey PRIMARY KEY (id); - - --- --- Name: sso_domains sso_domains_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.sso_domains - ADD CONSTRAINT sso_domains_pkey PRIMARY KEY (id); - - --- --- Name: sso_providers sso_providers_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.sso_providers - ADD CONSTRAINT sso_providers_pkey PRIMARY KEY (id); - - --- --- Name: users users_phone_key; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.users - ADD CONSTRAINT users_phone_key UNIQUE (phone); - - --- --- Name: users users_pkey; Type: CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.users - ADD CONSTRAINT users_pkey PRIMARY KEY (id); - - --- --- Name: buckets buckets_pkey; Type: CONSTRAINT; Schema: storage; Owner: supabase_storage_admin --- - -ALTER TABLE ONLY storage.buckets - ADD CONSTRAINT buckets_pkey PRIMARY KEY (id); - - --- --- Name: migrations migrations_name_key; Type: CONSTRAINT; Schema: storage; Owner: supabase_storage_admin --- - -ALTER TABLE ONLY storage.migrations - ADD CONSTRAINT migrations_name_key UNIQUE (name); - - --- --- Name: migrations migrations_pkey; Type: CONSTRAINT; Schema: storage; Owner: supabase_storage_admin --- - -ALTER TABLE ONLY storage.migrations - ADD CONSTRAINT migrations_pkey PRIMARY KEY (id); - - --- --- Name: objects objects_pkey; Type: CONSTRAINT; Schema: storage; Owner: supabase_storage_admin --- - -ALTER TABLE ONLY storage.objects - ADD CONSTRAINT objects_pkey PRIMARY KEY (id); - - --- --- Name: hooks hooks_pkey; Type: CONSTRAINT; Schema: supabase_functions; Owner: supabase_functions_admin --- - -ALTER TABLE ONLY supabase_functions.hooks - ADD CONSTRAINT hooks_pkey PRIMARY KEY (id); - - --- --- Name: migrations migrations_pkey; Type: CONSTRAINT; Schema: supabase_functions; Owner: supabase_functions_admin --- - -ALTER TABLE ONLY supabase_functions.migrations - ADD CONSTRAINT migrations_pkey PRIMARY KEY (version); - - --- --- Name: extensions_tenant_external_id_type_index; Type: INDEX; Schema: _realtime; Owner: postgres --- - -CREATE UNIQUE INDEX extensions_tenant_external_id_type_index ON _realtime.extensions USING btree (tenant_external_id, type); - - --- --- Name: tenants_external_id_index; Type: INDEX; Schema: _realtime; Owner: postgres --- - -CREATE UNIQUE INDEX tenants_external_id_index ON _realtime.tenants USING btree (external_id); - - --- --- Name: audit_logs_instance_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX audit_logs_instance_id_idx ON auth.audit_log_entries USING btree (instance_id); - - --- --- Name: confirmation_token_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE UNIQUE INDEX confirmation_token_idx ON auth.users USING btree (confirmation_token) WHERE ((confirmation_token)::text !~ '^[0-9 ]*$'::text); - - --- --- Name: email_change_token_current_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE UNIQUE INDEX email_change_token_current_idx ON auth.users USING btree (email_change_token_current) WHERE ((email_change_token_current)::text !~ '^[0-9 ]*$'::text); - - --- --- Name: email_change_token_new_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE UNIQUE INDEX email_change_token_new_idx ON auth.users USING btree (email_change_token_new) WHERE ((email_change_token_new)::text !~ '^[0-9 ]*$'::text); - - --- --- Name: factor_id_created_at_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX factor_id_created_at_idx ON auth.mfa_factors USING btree (user_id, created_at); - - --- --- Name: flow_state_created_at_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX flow_state_created_at_idx ON auth.flow_state USING btree (created_at DESC); - - --- --- Name: identities_email_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX identities_email_idx ON auth.identities USING btree (email text_pattern_ops); - - --- --- Name: INDEX identities_email_idx; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON INDEX auth.identities_email_idx IS 'Auth: Ensures indexed queries on the email column'; - - --- --- Name: identities_user_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX identities_user_id_idx ON auth.identities USING btree (user_id); - - --- --- Name: idx_auth_code; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX idx_auth_code ON auth.flow_state USING btree (auth_code); - - --- --- Name: idx_user_id_auth_method; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX idx_user_id_auth_method ON auth.flow_state USING btree (user_id, authentication_method); - - --- --- Name: mfa_challenge_created_at_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX mfa_challenge_created_at_idx ON auth.mfa_challenges USING btree (created_at DESC); - - --- --- Name: mfa_factors_user_friendly_name_unique; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE UNIQUE INDEX mfa_factors_user_friendly_name_unique ON auth.mfa_factors USING btree (friendly_name, user_id) WHERE (TRIM(BOTH FROM friendly_name) <> ''::text); - - --- --- Name: mfa_factors_user_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX mfa_factors_user_id_idx ON auth.mfa_factors USING btree (user_id); - - --- --- Name: reauthentication_token_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE UNIQUE INDEX reauthentication_token_idx ON auth.users USING btree (reauthentication_token) WHERE ((reauthentication_token)::text !~ '^[0-9 ]*$'::text); - - --- --- Name: recovery_token_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE UNIQUE INDEX recovery_token_idx ON auth.users USING btree (recovery_token) WHERE ((recovery_token)::text !~ '^[0-9 ]*$'::text); - - --- --- Name: refresh_tokens_instance_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX refresh_tokens_instance_id_idx ON auth.refresh_tokens USING btree (instance_id); - - --- --- Name: refresh_tokens_instance_id_user_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX refresh_tokens_instance_id_user_id_idx ON auth.refresh_tokens USING btree (instance_id, user_id); - - --- --- Name: refresh_tokens_parent_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX refresh_tokens_parent_idx ON auth.refresh_tokens USING btree (parent); - - --- --- Name: refresh_tokens_session_id_revoked_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX refresh_tokens_session_id_revoked_idx ON auth.refresh_tokens USING btree (session_id, revoked); - - --- --- Name: refresh_tokens_updated_at_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX refresh_tokens_updated_at_idx ON auth.refresh_tokens USING btree (updated_at DESC); - - --- --- Name: saml_providers_sso_provider_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX saml_providers_sso_provider_id_idx ON auth.saml_providers USING btree (sso_provider_id); - - --- --- Name: saml_relay_states_created_at_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX saml_relay_states_created_at_idx ON auth.saml_relay_states USING btree (created_at DESC); - - --- --- Name: saml_relay_states_for_email_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX saml_relay_states_for_email_idx ON auth.saml_relay_states USING btree (for_email); - - --- --- Name: saml_relay_states_sso_provider_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX saml_relay_states_sso_provider_id_idx ON auth.saml_relay_states USING btree (sso_provider_id); - - --- --- Name: sessions_not_after_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX sessions_not_after_idx ON auth.sessions USING btree (not_after DESC); - - --- --- Name: sessions_user_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX sessions_user_id_idx ON auth.sessions USING btree (user_id); - - --- --- Name: sso_domains_domain_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE UNIQUE INDEX sso_domains_domain_idx ON auth.sso_domains USING btree (lower(domain)); - - --- --- Name: sso_domains_sso_provider_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX sso_domains_sso_provider_id_idx ON auth.sso_domains USING btree (sso_provider_id); - - --- --- Name: sso_providers_resource_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE UNIQUE INDEX sso_providers_resource_id_idx ON auth.sso_providers USING btree (lower(resource_id)); - - --- --- Name: user_id_created_at_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX user_id_created_at_idx ON auth.sessions USING btree (user_id, created_at); - - --- --- Name: users_email_partial_key; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE UNIQUE INDEX users_email_partial_key ON auth.users USING btree (email) WHERE (is_sso_user = false); - - --- --- Name: INDEX users_email_partial_key; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin --- - -COMMENT ON INDEX auth.users_email_partial_key IS 'Auth: A partial unique index that applies only when is_sso_user is false'; - - --- --- Name: users_instance_id_email_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX users_instance_id_email_idx ON auth.users USING btree (instance_id, lower((email)::text)); - - --- --- Name: users_instance_id_idx; Type: INDEX; Schema: auth; Owner: supabase_auth_admin --- - -CREATE INDEX users_instance_id_idx ON auth.users USING btree (instance_id); - - --- --- Name: bname; Type: INDEX; Schema: storage; Owner: supabase_storage_admin --- - -CREATE UNIQUE INDEX bname ON storage.buckets USING btree (name); - - --- --- Name: bucketid_objname; Type: INDEX; Schema: storage; Owner: supabase_storage_admin --- - -CREATE UNIQUE INDEX bucketid_objname ON storage.objects USING btree (bucket_id, name); - - --- --- Name: name_prefix_search; Type: INDEX; Schema: storage; Owner: supabase_storage_admin --- - -CREATE INDEX name_prefix_search ON storage.objects USING btree (name text_pattern_ops); - - --- --- Name: supabase_functions_hooks_h_table_id_h_name_idx; Type: INDEX; Schema: supabase_functions; Owner: supabase_functions_admin --- - -CREATE INDEX supabase_functions_hooks_h_table_id_h_name_idx ON supabase_functions.hooks USING btree (hook_table_id, hook_name); - - --- --- Name: supabase_functions_hooks_request_id_idx; Type: INDEX; Schema: supabase_functions; Owner: supabase_functions_admin --- - -CREATE INDEX supabase_functions_hooks_request_id_idx ON supabase_functions.hooks USING btree (request_id); - - --- --- Name: objects update_objects_updated_at; Type: TRIGGER; Schema: storage; Owner: supabase_storage_admin --- - -CREATE TRIGGER update_objects_updated_at BEFORE UPDATE ON storage.objects FOR EACH ROW EXECUTE FUNCTION storage.update_updated_at_column(); - - --- --- Name: extensions extensions_tenant_external_id_fkey; Type: FK CONSTRAINT; Schema: _realtime; Owner: postgres --- - -ALTER TABLE ONLY _realtime.extensions - ADD CONSTRAINT extensions_tenant_external_id_fkey FOREIGN KEY (tenant_external_id) REFERENCES _realtime.tenants(external_id) ON DELETE CASCADE; - - --- --- Name: identities identities_user_id_fkey; Type: FK CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.identities - ADD CONSTRAINT identities_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE; - - --- --- Name: mfa_amr_claims mfa_amr_claims_session_id_fkey; Type: FK CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.mfa_amr_claims - ADD CONSTRAINT mfa_amr_claims_session_id_fkey FOREIGN KEY (session_id) REFERENCES auth.sessions(id) ON DELETE CASCADE; - - --- --- Name: mfa_challenges mfa_challenges_auth_factor_id_fkey; Type: FK CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.mfa_challenges - ADD CONSTRAINT mfa_challenges_auth_factor_id_fkey FOREIGN KEY (factor_id) REFERENCES auth.mfa_factors(id) ON DELETE CASCADE; - - --- --- Name: mfa_factors mfa_factors_user_id_fkey; Type: FK CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.mfa_factors - ADD CONSTRAINT mfa_factors_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE; - - --- --- Name: refresh_tokens refresh_tokens_session_id_fkey; Type: FK CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.refresh_tokens - ADD CONSTRAINT refresh_tokens_session_id_fkey FOREIGN KEY (session_id) REFERENCES auth.sessions(id) ON DELETE CASCADE; - - --- --- Name: saml_providers saml_providers_sso_provider_id_fkey; Type: FK CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.saml_providers - ADD CONSTRAINT saml_providers_sso_provider_id_fkey FOREIGN KEY (sso_provider_id) REFERENCES auth.sso_providers(id) ON DELETE CASCADE; - - --- --- Name: saml_relay_states saml_relay_states_flow_state_id_fkey; Type: FK CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.saml_relay_states - ADD CONSTRAINT saml_relay_states_flow_state_id_fkey FOREIGN KEY (flow_state_id) REFERENCES auth.flow_state(id) ON DELETE CASCADE; - - --- --- Name: saml_relay_states saml_relay_states_sso_provider_id_fkey; Type: FK CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.saml_relay_states - ADD CONSTRAINT saml_relay_states_sso_provider_id_fkey FOREIGN KEY (sso_provider_id) REFERENCES auth.sso_providers(id) ON DELETE CASCADE; - - --- --- Name: sessions sessions_user_id_fkey; Type: FK CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.sessions - ADD CONSTRAINT sessions_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE; - - --- --- Name: sso_domains sso_domains_sso_provider_id_fkey; Type: FK CONSTRAINT; Schema: auth; Owner: supabase_auth_admin --- - -ALTER TABLE ONLY auth.sso_domains - ADD CONSTRAINT sso_domains_sso_provider_id_fkey FOREIGN KEY (sso_provider_id) REFERENCES auth.sso_providers(id) ON DELETE CASCADE; - - --- --- Name: objects objects_bucketId_fkey; Type: FK CONSTRAINT; Schema: storage; Owner: supabase_storage_admin --- - -ALTER TABLE ONLY storage.objects - ADD CONSTRAINT "objects_bucketId_fkey" FOREIGN KEY (bucket_id) REFERENCES storage.buckets(id); - - --- --- Name: buckets; Type: ROW SECURITY; Schema: storage; Owner: supabase_storage_admin --- - -ALTER TABLE storage.buckets ENABLE ROW LEVEL SECURITY; - --- --- Name: migrations; Type: ROW SECURITY; Schema: storage; Owner: supabase_storage_admin --- - -ALTER TABLE storage.migrations ENABLE ROW LEVEL SECURITY; - --- --- Name: objects; Type: ROW SECURITY; Schema: storage; Owner: supabase_storage_admin --- - -ALTER TABLE storage.objects ENABLE ROW LEVEL SECURITY; - --- --- Name: supabase_realtime; Type: PUBLICATION; Schema: -; Owner: postgres --- - -CREATE PUBLICATION supabase_realtime WITH (publish = 'insert, update, delete, truncate'); - - -ALTER PUBLICATION supabase_realtime OWNER TO postgres; - --- --- Name: SCHEMA auth; Type: ACL; Schema: -; Owner: supabase_admin --- - -GRANT USAGE ON SCHEMA auth TO anon; -GRANT USAGE ON SCHEMA auth TO authenticated; -GRANT USAGE ON SCHEMA auth TO service_role; -GRANT ALL ON SCHEMA auth TO supabase_auth_admin; -GRANT ALL ON SCHEMA auth TO dashboard_user; -GRANT ALL ON SCHEMA auth TO postgres; - - --- --- Name: SCHEMA extensions; Type: ACL; Schema: -; Owner: postgres --- - -GRANT USAGE ON SCHEMA extensions TO anon; -GRANT USAGE ON SCHEMA extensions TO authenticated; -GRANT USAGE ON SCHEMA extensions TO service_role; -GRANT ALL ON SCHEMA extensions TO dashboard_user; - - --- --- Name: SCHEMA graphql_public; Type: ACL; Schema: -; Owner: supabase_admin --- - -GRANT USAGE ON SCHEMA graphql_public TO postgres; -GRANT USAGE ON SCHEMA graphql_public TO anon; -GRANT USAGE ON SCHEMA graphql_public TO authenticated; -GRANT USAGE ON SCHEMA graphql_public TO service_role; - - --- --- Name: SCHEMA net; Type: ACL; Schema: -; Owner: postgres --- - -GRANT USAGE ON SCHEMA net TO supabase_functions_admin; -GRANT USAGE ON SCHEMA net TO anon; -GRANT USAGE ON SCHEMA net TO authenticated; -GRANT USAGE ON SCHEMA net TO service_role; - - --- --- Name: SCHEMA public; Type: ACL; Schema: -; Owner: postgres --- - -REVOKE USAGE ON SCHEMA public FROM PUBLIC; -GRANT ALL ON SCHEMA public TO PUBLIC; -GRANT USAGE ON SCHEMA public TO anon; -GRANT USAGE ON SCHEMA public TO authenticated; -GRANT USAGE ON SCHEMA public TO service_role; - - --- --- Name: SCHEMA realtime; Type: ACL; Schema: -; Owner: supabase_admin --- - -GRANT USAGE ON SCHEMA realtime TO postgres; - - --- --- Name: SCHEMA storage; Type: ACL; Schema: -; Owner: supabase_admin --- - -GRANT ALL ON SCHEMA storage TO postgres; -GRANT USAGE ON SCHEMA storage TO anon; -GRANT USAGE ON SCHEMA storage TO authenticated; -GRANT USAGE ON SCHEMA storage TO service_role; -GRANT ALL ON SCHEMA storage TO supabase_storage_admin; -GRANT ALL ON SCHEMA storage TO dashboard_user; - - --- --- Name: SCHEMA supabase_functions; Type: ACL; Schema: -; Owner: supabase_admin --- - -GRANT USAGE ON SCHEMA supabase_functions TO postgres; -GRANT USAGE ON SCHEMA supabase_functions TO anon; -GRANT USAGE ON SCHEMA supabase_functions TO authenticated; -GRANT USAGE ON SCHEMA supabase_functions TO service_role; -GRANT ALL ON SCHEMA supabase_functions TO supabase_functions_admin; - - --- --- Name: FUNCTION email(); Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON FUNCTION auth.email() TO dashboard_user; - - --- --- Name: FUNCTION jwt(); Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON FUNCTION auth.jwt() TO postgres; -GRANT ALL ON FUNCTION auth.jwt() TO dashboard_user; - - --- --- Name: FUNCTION role(); Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON FUNCTION auth.role() TO dashboard_user; - - --- --- Name: FUNCTION uid(); Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON FUNCTION auth.uid() TO dashboard_user; - - --- --- Name: FUNCTION algorithm_sign(signables text, secret text, algorithm text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.algorithm_sign(signables text, secret text, algorithm text) TO dashboard_user; - - --- --- Name: FUNCTION armor(bytea); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.armor(bytea) TO dashboard_user; - - --- --- Name: FUNCTION armor(bytea, text[], text[]); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.armor(bytea, text[], text[]) TO dashboard_user; - - --- --- Name: FUNCTION crypt(text, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.crypt(text, text) TO dashboard_user; - - --- --- Name: FUNCTION dearmor(text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.dearmor(text) TO dashboard_user; - - --- --- Name: FUNCTION decrypt(bytea, bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.decrypt(bytea, bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION decrypt_iv(bytea, bytea, bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.decrypt_iv(bytea, bytea, bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION digest(bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.digest(bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION digest(text, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.digest(text, text) TO dashboard_user; - - --- --- Name: FUNCTION encrypt(bytea, bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.encrypt(bytea, bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION encrypt_iv(bytea, bytea, bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.encrypt_iv(bytea, bytea, bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION gen_random_bytes(integer); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.gen_random_bytes(integer) TO dashboard_user; - - --- --- Name: FUNCTION gen_random_uuid(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.gen_random_uuid() TO dashboard_user; - - --- --- Name: FUNCTION gen_salt(text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.gen_salt(text) TO dashboard_user; - - --- --- Name: FUNCTION gen_salt(text, integer); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.gen_salt(text, integer) TO dashboard_user; - - --- --- Name: FUNCTION grant_pg_cron_access(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.grant_pg_cron_access() TO dashboard_user; - - --- --- Name: FUNCTION grant_pg_net_access(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.grant_pg_net_access() TO dashboard_user; - - --- --- Name: FUNCTION hmac(bytea, bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.hmac(bytea, bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION hmac(text, text, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.hmac(text, text, text) TO dashboard_user; - - --- --- Name: FUNCTION pg_stat_statements(showtext boolean, OUT userid oid, OUT dbid oid, OUT toplevel boolean, OUT queryid bigint, OUT query text, OUT plans bigint, OUT total_plan_time double precision, OUT min_plan_time double precision, OUT max_plan_time double precision, OUT mean_plan_time double precision, OUT stddev_plan_time double precision, OUT calls bigint, OUT total_exec_time double precision, OUT min_exec_time double precision, OUT max_exec_time double precision, OUT mean_exec_time double precision, OUT stddev_exec_time double precision, OUT rows bigint, OUT shared_blks_hit bigint, OUT shared_blks_read bigint, OUT shared_blks_dirtied bigint, OUT shared_blks_written bigint, OUT local_blks_hit bigint, OUT local_blks_read bigint, OUT local_blks_dirtied bigint, OUT local_blks_written bigint, OUT temp_blks_read bigint, OUT temp_blks_written bigint, OUT blk_read_time double precision, OUT blk_write_time double precision, OUT wal_records bigint, OUT wal_fpi bigint, OUT wal_bytes numeric); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pg_stat_statements(showtext boolean, OUT userid oid, OUT dbid oid, OUT toplevel boolean, OUT queryid bigint, OUT query text, OUT plans bigint, OUT total_plan_time double precision, OUT min_plan_time double precision, OUT max_plan_time double precision, OUT mean_plan_time double precision, OUT stddev_plan_time double precision, OUT calls bigint, OUT total_exec_time double precision, OUT min_exec_time double precision, OUT max_exec_time double precision, OUT mean_exec_time double precision, OUT stddev_exec_time double precision, OUT rows bigint, OUT shared_blks_hit bigint, OUT shared_blks_read bigint, OUT shared_blks_dirtied bigint, OUT shared_blks_written bigint, OUT local_blks_hit bigint, OUT local_blks_read bigint, OUT local_blks_dirtied bigint, OUT local_blks_written bigint, OUT temp_blks_read bigint, OUT temp_blks_written bigint, OUT blk_read_time double precision, OUT blk_write_time double precision, OUT wal_records bigint, OUT wal_fpi bigint, OUT wal_bytes numeric) TO dashboard_user; - - --- --- Name: FUNCTION pg_stat_statements_info(OUT dealloc bigint, OUT stats_reset timestamp with time zone); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pg_stat_statements_info(OUT dealloc bigint, OUT stats_reset timestamp with time zone) TO dashboard_user; - - --- --- Name: FUNCTION pg_stat_statements_reset(userid oid, dbid oid, queryid bigint); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pg_stat_statements_reset(userid oid, dbid oid, queryid bigint) TO dashboard_user; - - --- --- Name: FUNCTION pgp_armor_headers(text, OUT key text, OUT value text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_armor_headers(text, OUT key text, OUT value text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_key_id(bytea); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_key_id(bytea) TO dashboard_user; - - --- --- Name: FUNCTION pgp_pub_decrypt(bytea, bytea); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_pub_decrypt(bytea, bytea) TO dashboard_user; - - --- --- Name: FUNCTION pgp_pub_decrypt(bytea, bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_pub_decrypt(bytea, bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_pub_decrypt(bytea, bytea, text, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_pub_decrypt(bytea, bytea, text, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_pub_decrypt_bytea(bytea, bytea); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_pub_decrypt_bytea(bytea, bytea) TO dashboard_user; - - --- --- Name: FUNCTION pgp_pub_decrypt_bytea(bytea, bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_pub_decrypt_bytea(bytea, bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_pub_decrypt_bytea(bytea, bytea, text, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_pub_decrypt_bytea(bytea, bytea, text, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_pub_encrypt(text, bytea); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_pub_encrypt(text, bytea) TO dashboard_user; - - --- --- Name: FUNCTION pgp_pub_encrypt(text, bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_pub_encrypt(text, bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_pub_encrypt_bytea(bytea, bytea); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_pub_encrypt_bytea(bytea, bytea) TO dashboard_user; - - --- --- Name: FUNCTION pgp_pub_encrypt_bytea(bytea, bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_pub_encrypt_bytea(bytea, bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_sym_decrypt(bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_sym_decrypt(bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_sym_decrypt(bytea, text, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_sym_decrypt(bytea, text, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_sym_decrypt_bytea(bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_sym_decrypt_bytea(bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_sym_decrypt_bytea(bytea, text, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_sym_decrypt_bytea(bytea, text, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_sym_encrypt(text, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_sym_encrypt(text, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_sym_encrypt(text, text, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_sym_encrypt(text, text, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_sym_encrypt_bytea(bytea, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_sym_encrypt_bytea(bytea, text) TO dashboard_user; - - --- --- Name: FUNCTION pgp_sym_encrypt_bytea(bytea, text, text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.pgp_sym_encrypt_bytea(bytea, text, text) TO dashboard_user; - - --- --- Name: FUNCTION sign(payload json, secret text, algorithm text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.sign(payload json, secret text, algorithm text) TO dashboard_user; - - --- --- Name: FUNCTION try_cast_double(inp text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.try_cast_double(inp text) TO dashboard_user; - - --- --- Name: FUNCTION url_decode(data text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.url_decode(data text) TO dashboard_user; - - --- --- Name: FUNCTION url_encode(data bytea); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.url_encode(data bytea) TO dashboard_user; - - --- --- Name: FUNCTION uuid_generate_v1(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.uuid_generate_v1() TO dashboard_user; - - --- --- Name: FUNCTION uuid_generate_v1mc(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.uuid_generate_v1mc() TO dashboard_user; - - --- --- Name: FUNCTION uuid_generate_v3(namespace uuid, name text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.uuid_generate_v3(namespace uuid, name text) TO dashboard_user; - - --- --- Name: FUNCTION uuid_generate_v4(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.uuid_generate_v4() TO dashboard_user; - - --- --- Name: FUNCTION uuid_generate_v5(namespace uuid, name text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.uuid_generate_v5(namespace uuid, name text) TO dashboard_user; - - --- --- Name: FUNCTION uuid_nil(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.uuid_nil() TO dashboard_user; - - --- --- Name: FUNCTION uuid_ns_dns(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.uuid_ns_dns() TO dashboard_user; - - --- --- Name: FUNCTION uuid_ns_oid(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.uuid_ns_oid() TO dashboard_user; - - --- --- Name: FUNCTION uuid_ns_url(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.uuid_ns_url() TO dashboard_user; - - --- --- Name: FUNCTION uuid_ns_x500(); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.uuid_ns_x500() TO dashboard_user; - - --- --- Name: FUNCTION verify(token text, secret text, algorithm text); Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON FUNCTION extensions.verify(token text, secret text, algorithm text) TO dashboard_user; - - --- --- Name: FUNCTION comment_directive(comment_ text); Type: ACL; Schema: graphql; Owner: supabase_admin --- - -GRANT ALL ON FUNCTION graphql.comment_directive(comment_ text) TO postgres; -GRANT ALL ON FUNCTION graphql.comment_directive(comment_ text) TO anon; -GRANT ALL ON FUNCTION graphql.comment_directive(comment_ text) TO authenticated; -GRANT ALL ON FUNCTION graphql.comment_directive(comment_ text) TO service_role; - - --- --- Name: FUNCTION exception(message text); Type: ACL; Schema: graphql; Owner: supabase_admin --- - -GRANT ALL ON FUNCTION graphql.exception(message text) TO postgres; -GRANT ALL ON FUNCTION graphql.exception(message text) TO anon; -GRANT ALL ON FUNCTION graphql.exception(message text) TO authenticated; -GRANT ALL ON FUNCTION graphql.exception(message text) TO service_role; - - --- --- Name: FUNCTION get_schema_version(); Type: ACL; Schema: graphql; Owner: supabase_admin --- - -GRANT ALL ON FUNCTION graphql.get_schema_version() TO postgres; -GRANT ALL ON FUNCTION graphql.get_schema_version() TO anon; -GRANT ALL ON FUNCTION graphql.get_schema_version() TO authenticated; -GRANT ALL ON FUNCTION graphql.get_schema_version() TO service_role; - - --- --- Name: FUNCTION increment_schema_version(); Type: ACL; Schema: graphql; Owner: supabase_admin --- - -GRANT ALL ON FUNCTION graphql.increment_schema_version() TO postgres; -GRANT ALL ON FUNCTION graphql.increment_schema_version() TO anon; -GRANT ALL ON FUNCTION graphql.increment_schema_version() TO authenticated; -GRANT ALL ON FUNCTION graphql.increment_schema_version() TO service_role; - - --- --- Name: FUNCTION sequential_executor(prepared_statement_names text[]); Type: ACL; Schema: graphql; Owner: supabase_admin --- - -GRANT ALL ON FUNCTION graphql.sequential_executor(prepared_statement_names text[]) TO postgres; -GRANT ALL ON FUNCTION graphql.sequential_executor(prepared_statement_names text[]) TO anon; -GRANT ALL ON FUNCTION graphql.sequential_executor(prepared_statement_names text[]) TO authenticated; -GRANT ALL ON FUNCTION graphql.sequential_executor(prepared_statement_names text[]) TO service_role; - - --- --- Name: FUNCTION graphql("operationName" text, query text, variables jsonb, extensions jsonb); Type: ACL; Schema: graphql_public; Owner: supabase_admin --- - --- GRANT ALL ON FUNCTION graphql_public.graphql("operationName" text, query text, variables jsonb, extensions jsonb) TO postgres; --- GRANT ALL ON FUNCTION graphql_public.graphql("operationName" text, query text, variables jsonb, extensions jsonb) TO anon; --- GRANT ALL ON FUNCTION graphql_public.graphql("operationName" text, query text, variables jsonb, extensions jsonb) TO authenticated; --- GRANT ALL ON FUNCTION graphql_public.graphql("operationName" text, query text, variables jsonb, extensions jsonb) TO service_role; - - --- --- Name: FUNCTION http_collect_response(request_id bigint, async boolean); Type: ACL; Schema: net; Owner: supabase_admin --- - -REVOKE ALL ON FUNCTION net.http_collect_response(request_id bigint, async boolean) FROM PUBLIC; -GRANT ALL ON FUNCTION net.http_collect_response(request_id bigint, async boolean) TO supabase_functions_admin; -GRANT ALL ON FUNCTION net.http_collect_response(request_id bigint, async boolean) TO postgres; -GRANT ALL ON FUNCTION net.http_collect_response(request_id bigint, async boolean) TO anon; -GRANT ALL ON FUNCTION net.http_collect_response(request_id bigint, async boolean) TO authenticated; -GRANT ALL ON FUNCTION net.http_collect_response(request_id bigint, async boolean) TO service_role; - - --- --- Name: FUNCTION http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer); Type: ACL; Schema: net; Owner: supabase_admin --- - -REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; -GRANT ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin; -GRANT ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO postgres; -GRANT ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO anon; -GRANT ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO authenticated; -GRANT ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO service_role; - - --- --- Name: FUNCTION http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer); Type: ACL; Schema: net; Owner: supabase_admin --- - -REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; -GRANT ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin; -GRANT ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO postgres; -GRANT ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO anon; -GRANT ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO authenticated; -GRANT ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO service_role; - - --- --- Name: FUNCTION get_auth(p_usename text); Type: ACL; Schema: pgbouncer; Owner: postgres --- - -REVOKE ALL ON FUNCTION pgbouncer.get_auth(p_usename text) FROM PUBLIC; -GRANT ALL ON FUNCTION pgbouncer.get_auth(p_usename text) TO pgbouncer; - - --- --- Name: SEQUENCE key_key_id_seq; Type: ACL; Schema: pgsodium; Owner: postgres --- - -GRANT ALL ON SEQUENCE pgsodium.key_key_id_seq TO pgsodium_keyiduser; - - --- --- Name: FUNCTION extension(name text); Type: ACL; Schema: storage; Owner: supabase_storage_admin --- - -GRANT ALL ON FUNCTION storage.extension(name text) TO anon; -GRANT ALL ON FUNCTION storage.extension(name text) TO authenticated; -GRANT ALL ON FUNCTION storage.extension(name text) TO service_role; -GRANT ALL ON FUNCTION storage.extension(name text) TO dashboard_user; -GRANT ALL ON FUNCTION storage.extension(name text) TO postgres; - - --- --- Name: FUNCTION filename(name text); Type: ACL; Schema: storage; Owner: supabase_storage_admin --- - -GRANT ALL ON FUNCTION storage.filename(name text) TO anon; -GRANT ALL ON FUNCTION storage.filename(name text) TO authenticated; -GRANT ALL ON FUNCTION storage.filename(name text) TO service_role; -GRANT ALL ON FUNCTION storage.filename(name text) TO dashboard_user; -GRANT ALL ON FUNCTION storage.filename(name text) TO postgres; - - --- --- Name: FUNCTION foldername(name text); Type: ACL; Schema: storage; Owner: supabase_storage_admin --- - -GRANT ALL ON FUNCTION storage.foldername(name text) TO anon; -GRANT ALL ON FUNCTION storage.foldername(name text) TO authenticated; -GRANT ALL ON FUNCTION storage.foldername(name text) TO service_role; -GRANT ALL ON FUNCTION storage.foldername(name text) TO dashboard_user; -GRANT ALL ON FUNCTION storage.foldername(name text) TO postgres; - - --- --- Name: FUNCTION http_request(); Type: ACL; Schema: supabase_functions; Owner: supabase_functions_admin --- - -REVOKE ALL ON FUNCTION supabase_functions.http_request() FROM PUBLIC; -GRANT ALL ON FUNCTION supabase_functions.http_request() TO postgres; -GRANT ALL ON FUNCTION supabase_functions.http_request() TO anon; -GRANT ALL ON FUNCTION supabase_functions.http_request() TO authenticated; -GRANT ALL ON FUNCTION supabase_functions.http_request() TO service_role; - - --- --- Name: TABLE audit_log_entries; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.audit_log_entries TO dashboard_user; -GRANT ALL ON TABLE auth.audit_log_entries TO postgres; - - --- --- Name: TABLE flow_state; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.flow_state TO postgres; -GRANT ALL ON TABLE auth.flow_state TO dashboard_user; - - --- --- Name: TABLE identities; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.identities TO postgres; -GRANT ALL ON TABLE auth.identities TO dashboard_user; - - --- --- Name: TABLE instances; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.instances TO dashboard_user; -GRANT ALL ON TABLE auth.instances TO postgres; - - --- --- Name: TABLE mfa_amr_claims; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.mfa_amr_claims TO postgres; -GRANT ALL ON TABLE auth.mfa_amr_claims TO dashboard_user; - - --- --- Name: TABLE mfa_challenges; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.mfa_challenges TO postgres; -GRANT ALL ON TABLE auth.mfa_challenges TO dashboard_user; - - --- --- Name: TABLE mfa_factors; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.mfa_factors TO postgres; -GRANT ALL ON TABLE auth.mfa_factors TO dashboard_user; - - --- --- Name: TABLE refresh_tokens; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.refresh_tokens TO dashboard_user; -GRANT ALL ON TABLE auth.refresh_tokens TO postgres; - - --- --- Name: SEQUENCE refresh_tokens_id_seq; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON SEQUENCE auth.refresh_tokens_id_seq TO dashboard_user; -GRANT ALL ON SEQUENCE auth.refresh_tokens_id_seq TO postgres; - - --- --- Name: TABLE saml_providers; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.saml_providers TO postgres; -GRANT ALL ON TABLE auth.saml_providers TO dashboard_user; - - --- --- Name: TABLE saml_relay_states; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.saml_relay_states TO postgres; -GRANT ALL ON TABLE auth.saml_relay_states TO dashboard_user; - - --- --- Name: TABLE schema_migrations; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.schema_migrations TO dashboard_user; -GRANT ALL ON TABLE auth.schema_migrations TO postgres; - - --- --- Name: TABLE sessions; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.sessions TO postgres; -GRANT ALL ON TABLE auth.sessions TO dashboard_user; - - --- --- Name: TABLE sso_domains; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.sso_domains TO postgres; -GRANT ALL ON TABLE auth.sso_domains TO dashboard_user; - - --- --- Name: TABLE sso_providers; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.sso_providers TO postgres; -GRANT ALL ON TABLE auth.sso_providers TO dashboard_user; - - --- --- Name: TABLE users; Type: ACL; Schema: auth; Owner: supabase_auth_admin --- - -GRANT ALL ON TABLE auth.users TO dashboard_user; -GRANT ALL ON TABLE auth.users TO postgres; - - --- --- Name: TABLE pg_stat_statements; Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON TABLE extensions.pg_stat_statements TO dashboard_user; - - --- --- Name: TABLE pg_stat_statements_info; Type: ACL; Schema: extensions; Owner: postgres --- - -GRANT ALL ON TABLE extensions.pg_stat_statements_info TO dashboard_user; - - --- --- Name: SEQUENCE seq_schema_version; Type: ACL; Schema: graphql; Owner: supabase_admin --- - -GRANT ALL ON SEQUENCE graphql.seq_schema_version TO postgres; -GRANT ALL ON SEQUENCE graphql.seq_schema_version TO anon; -GRANT ALL ON SEQUENCE graphql.seq_schema_version TO authenticated; -GRANT ALL ON SEQUENCE graphql.seq_schema_version TO service_role; - - --- --- Name: TABLE valid_key; Type: ACL; Schema: pgsodium; Owner: postgres --- - -GRANT ALL ON TABLE pgsodium.valid_key TO pgsodium_keyiduser; - - --- --- Name: TABLE buckets; Type: ACL; Schema: storage; Owner: supabase_storage_admin --- - -GRANT ALL ON TABLE storage.buckets TO anon; -GRANT ALL ON TABLE storage.buckets TO authenticated; -GRANT ALL ON TABLE storage.buckets TO service_role; -GRANT ALL ON TABLE storage.buckets TO postgres; - - --- --- Name: TABLE migrations; Type: ACL; Schema: storage; Owner: supabase_storage_admin --- - -GRANT ALL ON TABLE storage.migrations TO anon; -GRANT ALL ON TABLE storage.migrations TO authenticated; -GRANT ALL ON TABLE storage.migrations TO service_role; -GRANT ALL ON TABLE storage.migrations TO postgres; - - --- --- Name: TABLE objects; Type: ACL; Schema: storage; Owner: supabase_storage_admin --- - -GRANT ALL ON TABLE storage.objects TO anon; -GRANT ALL ON TABLE storage.objects TO authenticated; -GRANT ALL ON TABLE storage.objects TO service_role; -GRANT ALL ON TABLE storage.objects TO postgres; - - --- --- Name: TABLE hooks; Type: ACL; Schema: supabase_functions; Owner: supabase_functions_admin --- - -GRANT ALL ON TABLE supabase_functions.hooks TO postgres; -GRANT ALL ON TABLE supabase_functions.hooks TO anon; -GRANT ALL ON TABLE supabase_functions.hooks TO authenticated; -GRANT ALL ON TABLE supabase_functions.hooks TO service_role; - - --- --- Name: SEQUENCE hooks_id_seq; Type: ACL; Schema: supabase_functions; Owner: supabase_functions_admin --- - -GRANT ALL ON SEQUENCE supabase_functions.hooks_id_seq TO postgres; -GRANT ALL ON SEQUENCE supabase_functions.hooks_id_seq TO anon; -GRANT ALL ON SEQUENCE supabase_functions.hooks_id_seq TO authenticated; -GRANT ALL ON SEQUENCE supabase_functions.hooks_id_seq TO service_role; - - --- --- Name: TABLE migrations; Type: ACL; Schema: supabase_functions; Owner: supabase_functions_admin --- - -GRANT ALL ON TABLE supabase_functions.migrations TO postgres; -GRANT ALL ON TABLE supabase_functions.migrations TO anon; -GRANT ALL ON TABLE supabase_functions.migrations TO authenticated; -GRANT ALL ON TABLE supabase_functions.migrations TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR SEQUENCES; Type: DEFAULT ACL; Schema: auth; Owner: supabase_auth_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_auth_admin IN SCHEMA auth GRANT ALL ON SEQUENCES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_auth_admin IN SCHEMA auth GRANT ALL ON SEQUENCES TO dashboard_user; - - --- --- Name: DEFAULT PRIVILEGES FOR FUNCTIONS; Type: DEFAULT ACL; Schema: auth; Owner: supabase_auth_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_auth_admin IN SCHEMA auth GRANT ALL ON FUNCTIONS TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_auth_admin IN SCHEMA auth GRANT ALL ON FUNCTIONS TO dashboard_user; - - --- --- Name: DEFAULT PRIVILEGES FOR TABLES; Type: DEFAULT ACL; Schema: auth; Owner: supabase_auth_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_auth_admin IN SCHEMA auth GRANT ALL ON TABLES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_auth_admin IN SCHEMA auth GRANT ALL ON TABLES TO dashboard_user; - - --- --- Name: DEFAULT PRIVILEGES FOR SEQUENCES; Type: DEFAULT ACL; Schema: graphql; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql GRANT ALL ON SEQUENCES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql GRANT ALL ON SEQUENCES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql GRANT ALL ON SEQUENCES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql GRANT ALL ON SEQUENCES TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR SEQUENCES; Type: DEFAULT ACL; Schema: graphql; Owner: postgres --- - -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA graphql GRANT ALL ON SEQUENCES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA graphql GRANT ALL ON SEQUENCES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA graphql GRANT ALL ON SEQUENCES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA graphql GRANT ALL ON SEQUENCES TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR FUNCTIONS; Type: DEFAULT ACL; Schema: graphql; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql GRANT ALL ON FUNCTIONS TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql GRANT ALL ON FUNCTIONS TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql GRANT ALL ON FUNCTIONS TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql GRANT ALL ON FUNCTIONS TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR FUNCTIONS; Type: DEFAULT ACL; Schema: graphql; Owner: postgres --- - -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA graphql GRANT ALL ON FUNCTIONS TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA graphql GRANT ALL ON FUNCTIONS TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA graphql GRANT ALL ON FUNCTIONS TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA graphql GRANT ALL ON FUNCTIONS TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR TABLES; Type: DEFAULT ACL; Schema: graphql; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql GRANT ALL ON TABLES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql GRANT ALL ON TABLES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql GRANT ALL ON TABLES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql GRANT ALL ON TABLES TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR TABLES; Type: DEFAULT ACL; Schema: graphql; Owner: postgres --- - -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA graphql GRANT ALL ON TABLES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA graphql GRANT ALL ON TABLES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA graphql GRANT ALL ON TABLES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA graphql GRANT ALL ON TABLES TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR SEQUENCES; Type: DEFAULT ACL; Schema: graphql_public; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON SEQUENCES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON SEQUENCES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON SEQUENCES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON SEQUENCES TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR FUNCTIONS; Type: DEFAULT ACL; Schema: graphql_public; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON FUNCTIONS TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON FUNCTIONS TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON FUNCTIONS TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON FUNCTIONS TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR TABLES; Type: DEFAULT ACL; Schema: graphql_public; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON TABLES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON TABLES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON TABLES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA graphql_public GRANT ALL ON TABLES TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR SEQUENCES; Type: DEFAULT ACL; Schema: pgsodium; Owner: postgres --- - -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA pgsodium GRANT ALL ON SEQUENCES TO pgsodium_keyiduser; - - --- --- Name: DEFAULT PRIVILEGES FOR TABLES; Type: DEFAULT ACL; Schema: pgsodium; Owner: postgres --- - -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA pgsodium GRANT ALL ON TABLES TO pgsodium_keyiduser; - - --- --- Name: DEFAULT PRIVILEGES FOR SEQUENCES; Type: DEFAULT ACL; Schema: public; Owner: postgres --- - -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR SEQUENCES; Type: DEFAULT ACL; Schema: public; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR FUNCTIONS; Type: DEFAULT ACL; Schema: public; Owner: postgres --- - -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR FUNCTIONS; Type: DEFAULT ACL; Schema: public; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR TABLES; Type: DEFAULT ACL; Schema: public; Owner: postgres --- - -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR TABLES; Type: DEFAULT ACL; Schema: public; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR SEQUENCES; Type: DEFAULT ACL; Schema: realtime; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA realtime GRANT ALL ON SEQUENCES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA realtime GRANT ALL ON SEQUENCES TO dashboard_user; - - --- --- Name: DEFAULT PRIVILEGES FOR FUNCTIONS; Type: DEFAULT ACL; Schema: realtime; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA realtime GRANT ALL ON FUNCTIONS TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA realtime GRANT ALL ON FUNCTIONS TO dashboard_user; - - --- --- Name: DEFAULT PRIVILEGES FOR TABLES; Type: DEFAULT ACL; Schema: realtime; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA realtime GRANT ALL ON TABLES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA realtime GRANT ALL ON TABLES TO dashboard_user; - - --- --- Name: DEFAULT PRIVILEGES FOR SEQUENCES; Type: DEFAULT ACL; Schema: storage; Owner: postgres --- - -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON SEQUENCES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON SEQUENCES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON SEQUENCES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON SEQUENCES TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR FUNCTIONS; Type: DEFAULT ACL; Schema: storage; Owner: postgres --- - -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON FUNCTIONS TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON FUNCTIONS TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON FUNCTIONS TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON FUNCTIONS TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR TABLES; Type: DEFAULT ACL; Schema: storage; Owner: postgres --- - -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON TABLES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON TABLES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON TABLES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA storage GRANT ALL ON TABLES TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR SEQUENCES; Type: DEFAULT ACL; Schema: supabase_functions; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR FUNCTIONS; Type: DEFAULT ACL; Schema: supabase_functions; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO service_role; - - --- --- Name: DEFAULT PRIVILEGES FOR TABLES; Type: DEFAULT ACL; Schema: supabase_functions; Owner: supabase_admin --- - -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA supabase_functions GRANT ALL ON TABLES TO postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA supabase_functions GRANT ALL ON TABLES TO anon; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA supabase_functions GRANT ALL ON TABLES TO authenticated; -ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA supabase_functions GRANT ALL ON TABLES TO service_role; - - --- --- Name: issue_graphql_placeholder; Type: EVENT TRIGGER; Schema: -; Owner: supabase_admin --- - -CREATE EVENT TRIGGER issue_graphql_placeholder ON sql_drop - WHEN TAG IN ('DROP EXTENSION') - EXECUTE FUNCTION extensions.set_graphql_placeholder(); - - -ALTER EVENT TRIGGER issue_graphql_placeholder OWNER TO supabase_admin; - --- --- Name: issue_pg_cron_access; Type: EVENT TRIGGER; Schema: -; Owner: supabase_admin --- - -CREATE EVENT TRIGGER issue_pg_cron_access ON ddl_command_end - WHEN TAG IN ('CREATE SCHEMA') - EXECUTE FUNCTION extensions.grant_pg_cron_access(); - - -ALTER EVENT TRIGGER issue_pg_cron_access OWNER TO supabase_admin; - --- --- Name: issue_pg_graphql_access; Type: EVENT TRIGGER; Schema: -; Owner: supabase_admin --- - -CREATE EVENT TRIGGER issue_pg_graphql_access ON ddl_command_end - WHEN TAG IN ('CREATE FUNCTION') - EXECUTE FUNCTION extensions.grant_pg_graphql_access(); - - -ALTER EVENT TRIGGER issue_pg_graphql_access OWNER TO supabase_admin; - --- --- Name: issue_pg_net_access; Type: EVENT TRIGGER; Schema: -; Owner: supabase_admin --- - -CREATE EVENT TRIGGER issue_pg_net_access ON ddl_command_end - WHEN TAG IN ('CREATE EXTENSION') - EXECUTE FUNCTION extensions.grant_pg_net_access(); - - -ALTER EVENT TRIGGER issue_pg_net_access OWNER TO supabase_admin; - --- --- Name: pgrst_ddl_watch; Type: EVENT TRIGGER; Schema: -; Owner: supabase_admin --- - -CREATE EVENT TRIGGER pgrst_ddl_watch ON ddl_command_end - EXECUTE FUNCTION extensions.pgrst_ddl_watch(); - - -ALTER EVENT TRIGGER pgrst_ddl_watch OWNER TO supabase_admin; - --- --- Name: pgrst_drop_watch; Type: EVENT TRIGGER; Schema: -; Owner: supabase_admin --- - -CREATE EVENT TRIGGER pgrst_drop_watch ON sql_drop - EXECUTE FUNCTION extensions.pgrst_drop_watch(); - - -ALTER EVENT TRIGGER pgrst_drop_watch OWNER TO supabase_admin; - --- --- PostgreSQL database dump complete --- - -DROP EXTENSION pg_graphql; CREATE EXTENSION pg_graphql WITH SCHEMA graphql; -RESET ALL; diff --git a/apps/cli-go/internal/utils/templates/initial_schemas/README.md b/apps/cli-go/internal/utils/templates/initial_schemas/README.md deleted file mode 100644 index 77cfae04c6..0000000000 --- a/apps/cli-go/internal/utils/templates/initial_schemas/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# About Initial Schemas - -These SQL files represent initial schemas needed to set up the database with Supabase stuff. These need to be manually generated for each Postgres major version. Which initial schema used depends on the Docker image tag used to run the local db, which in turn depends on the CLI's `db.major_version` config. - -The initial schema for PG12 is not available because the latest image (`supabase/postgres:12.5.0`) doesn't contain `wal2json`, which is required for Realtime to work. - -# Why use the pg_dump output instead of running the `init.sql` directly? - -Because Realtime, GoTrue, Logflare, and Storage have their own migrations, and these need to be included in the initial schema for e.g. `supabase db reset` to work. - -# How to Generate Initial Schemas - -1. Start supabase local development stack with default config - -```bash -go run . init -go run . start -x gotrue,storage-api,imgproxy -``` - -2. Run the initial dump script and pipe output to `15.sql` - -```bash -./tools/dump_initial_schema.sh > internal/utils/templates/initial_schemas/15.sql -``` - -3. Commit changes that are relevant for the update - -- `INSERT INTO _realtime.extensions` and `_realtime.tenants` usually do not require updating. -- `INSERT INTO *.schema_migrations` statements are only required if there are new migrations. -- `ALTER EVENT TRIGGER issue_pg_cron_access OWNER TO` should be followed by `supabase_admin`. - -4. Set `major_version = 14` in `supabase/config.toml` and repeat steps 1-3 to update `14.sql` -5. Set `major_version = 13` in `supabase/config.toml` and repeat steps 1-3 to update `13.sql` diff --git a/apps/cli-go/internal/utils/tenant/client.go b/apps/cli-go/internal/utils/tenant/client.go deleted file mode 100644 index 912387739e..0000000000 --- a/apps/cli-go/internal/utils/tenant/client.go +++ /dev/null @@ -1,97 +0,0 @@ -package tenant - -import ( - "context" - "strings" - - "github.com/go-errors/errors" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/cast" - "github.com/supabase/cli/pkg/fetcher" -) - -var ( - ErrAuthToken = errors.New("Authorization failed for the access token and project ref pair") - errMissingKey = errors.New("Anon key not found.") -) - -type ApiKey struct { - Anon string - ServiceRole string -} - -func (a ApiKey) IsEmpty() bool { - return len(a.Anon) == 0 && len(a.ServiceRole) == 0 -} - -func NewApiKey(resp []api.ApiKeyResponseOutput) ApiKey { - var result ApiKey - for _, key := range resp { - value, err := key.ApiKey.Get() - if err != nil { - continue - } - if t, err := key.Type.Get(); err == nil { - switch t { - case api.ApiKeyResponseOutputTypePublishable: - result.Anon = value - continue - case api.ApiKeyResponseOutputTypeSecret: - if isServiceRole(key) { - result.ServiceRole = value - } - continue - } - } - switch key.Name { - case "anon": - if len(result.Anon) == 0 { - result.Anon = value - } - case "service_role": - if len(result.ServiceRole) == 0 { - result.ServiceRole = value - } - } - } - return result -} - -func isServiceRole(key api.ApiKeyResponseOutput) bool { - if tmpl, err := key.SecretJwtTemplate.Get(); err == nil { - if role, ok := tmpl["role"].(string); ok { - return strings.EqualFold(role, "service_role") - } - } - return false -} - -func GetApiKeys(ctx context.Context, projectRef string) (ApiKey, error) { - resp, err := utils.GetSupabase().V1GetProjectApiKeysWithResponse(ctx, projectRef, &api.V1GetProjectApiKeysParams{ - Reveal: cast.Ptr("true"), - }) - if err != nil { - return ApiKey{}, errors.Errorf("failed to get api keys: %w", err) - } - if resp.JSON200 == nil { - return ApiKey{}, errors.Errorf("%w: %s", ErrAuthToken, string(resp.Body)) - } - keys := NewApiKey(*resp.JSON200) - if keys.IsEmpty() { - return ApiKey{}, errors.New(errMissingKey) - } - return keys, nil -} - -type TenantAPI struct { - *fetcher.Fetcher -} - -func NewTenantAPI(ctx context.Context, projectRef, serviceKey string) TenantAPI { - return TenantAPI{Fetcher: fetcher.NewServiceGateway( - "https://"+utils.GetSupabaseHost(projectRef), - serviceKey, - fetcher.WithUserAgent("SupabaseCLI/"+utils.Version), - )} -} diff --git a/apps/cli-go/internal/utils/tenant/client_test.go b/apps/cli-go/internal/utils/tenant/client_test.go deleted file mode 100644 index ac29cfc403..0000000000 --- a/apps/cli-go/internal/utils/tenant/client_test.go +++ /dev/null @@ -1,151 +0,0 @@ -package tenant - -import ( - "context" - "net/http" - "testing" - - "github.com/h2non/gock" - "github.com/oapi-codegen/nullable" - "github.com/stretchr/testify/assert" - "github.com/supabase/cli/internal/testing/apitest" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/api" -) - -func TestApiKey(t *testing.T) { - t.Run("creates api key from response", func(t *testing.T) { - resp := []api.ApiKeyResponseOutput{ - {Name: "anon", ApiKey: nullable.NewNullableWithValue("anon-key")}, - {Name: "service_role", ApiKey: nullable.NewNullableWithValue("service-key")}, - } - - keys := NewApiKey(resp) - - assert.Equal(t, "anon-key", keys.Anon) - assert.Equal(t, "service-key", keys.ServiceRole) - assert.False(t, keys.IsEmpty()) - }) - - t.Run("handles empty response", func(t *testing.T) { - resp := []api.ApiKeyResponseOutput{ - {Name: "service_role", ApiKey: nullable.NewNullNullable[string]()}, - } - - keys := NewApiKey(resp) - - assert.Empty(t, keys.Anon) - assert.Empty(t, keys.ServiceRole) - assert.True(t, keys.IsEmpty()) - }) - - t.Run("handles partial response", func(t *testing.T) { - resp := []api.ApiKeyResponseOutput{ - {Name: "anon", ApiKey: nullable.NewNullableWithValue("anon-key")}, - } - - keys := NewApiKey(resp) - - assert.Equal(t, "anon-key", keys.Anon) - assert.Empty(t, keys.ServiceRole) - assert.False(t, keys.IsEmpty()) - }) -} - -func TestGetApiKeys(t *testing.T) { - t.Run("retrieves api keys successfully", func(t *testing.T) { - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) - - defer gock.OffAll() - projectRef := apitest.RandomProjectRef() - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + projectRef + "/api-keys"). - Reply(http.StatusOK). - JSON([]api.ApiKeyResponseOutput{ - {Name: "anon", ApiKey: nullable.NewNullableWithValue("anon-key")}, - {Name: "service_role", ApiKey: nullable.NewNullableWithValue("service-key")}, - }) - - keys, err := GetApiKeys(context.Background(), projectRef) - - assert.NoError(t, err) - assert.Equal(t, "anon-key", keys.Anon) - assert.Equal(t, "service-key", keys.ServiceRole) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("handles network error", func(t *testing.T) { - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) - - defer gock.OffAll() - projectRef := apitest.RandomProjectRef() - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + projectRef + "/api-keys"). - ReplyError(gock.ErrCannotMatch) - - keys, err := GetApiKeys(context.Background(), projectRef) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to get api keys") - assert.True(t, keys.IsEmpty()) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("handles unauthorized error", func(t *testing.T) { - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) - - defer gock.OffAll() - projectRef := apitest.RandomProjectRef() - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + projectRef + "/api-keys"). - Reply(http.StatusUnauthorized). - JSON(map[string]string{"message": "unauthorized"}) - - keys, err := GetApiKeys(context.Background(), projectRef) - - assert.ErrorIs(t, err, ErrAuthToken) - assert.True(t, keys.IsEmpty()) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("handles missing anon key", func(t *testing.T) { - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) - - defer gock.OffAll() - projectRef := apitest.RandomProjectRef() - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + projectRef + "/api-keys"). - Reply(http.StatusOK). - JSON([]api.ApiKeyResponseOutput{}) // should this error if response has only service_role key? - - keys, err := GetApiKeys(context.Background(), projectRef) - - assert.ErrorIs(t, err, errMissingKey) - assert.True(t, keys.IsEmpty()) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} - -func TestNewTenantAPI(t *testing.T) { - projectRef := apitest.RandomProjectRef() - anonKey := "test-key" - - api := NewTenantAPI(context.Background(), projectRef, anonKey) - assert.NotNil(t, api.Fetcher) - - defer gock.OffAll() - gock.New("https://"+utils.GetSupabaseHost(projectRef)). - Get("/test"). - MatchHeader("Authorization", "Bearer "+anonKey). - MatchHeader("User-Agent", "SupabaseCLI/"+utils.Version). - Reply(http.StatusOK) - - _, err := api.Send(context.Background(), http.MethodGet, "/test", nil) - - assert.NoError(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) -} diff --git a/apps/cli-go/internal/utils/tenant/database.go b/apps/cli-go/internal/utils/tenant/database.go deleted file mode 100644 index ababf74af2..0000000000 --- a/apps/cli-go/internal/utils/tenant/database.go +++ /dev/null @@ -1,24 +0,0 @@ -package tenant - -import ( - "context" - - "github.com/go-errors/errors" - "github.com/supabase/cli/internal/utils" -) - -var errDatabaseVersion = errors.New("Database version not found.") - -func GetDatabaseVersion(ctx context.Context, projectRef string) (string, error) { - resp, err := utils.GetSupabase().V1GetProjectWithResponse(ctx, projectRef) - if err != nil { - return "", errors.Errorf("failed to retrieve project: %w", err) - } - if resp.JSON200 == nil { - return "", errors.Errorf("unexpected retrieve project status %d: %s", resp.StatusCode(), string(resp.Body)) - } - if len(resp.JSON200.Database.Version) > 0 { - return resp.JSON200.Database.Version, nil - } - return "", errors.New(errDatabaseVersion) -} diff --git a/apps/cli-go/internal/utils/tenant/database_test.go b/apps/cli-go/internal/utils/tenant/database_test.go deleted file mode 100644 index 5fbbdadb1a..0000000000 --- a/apps/cli-go/internal/utils/tenant/database_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package tenant - -import ( - "context" - "net/http" - "testing" - - "github.com/h2non/gock" - "github.com/stretchr/testify/assert" - "github.com/supabase/cli/internal/testing/apitest" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/api" -) - -func TestGetDatabaseVersion(t *testing.T) { - // Setup valid access token - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) - // Setup valid project ref - projectRef := apitest.RandomProjectRef() - - t.Run("retrieves database version successfully", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - mockPostgres := api.V1ProjectWithDatabaseResponseOutput{} - mockPostgres.Database.Version = "14.1.0.99" - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + projectRef). - Reply(http.StatusOK). - JSON(mockPostgres) - // Run test - version, err := GetDatabaseVersion(context.Background(), projectRef) - // Check error - assert.NoError(t, err) - assert.Equal(t, "14.1.0.99", version) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("handles project not found", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - projectRef := apitest.RandomProjectRef() - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + projectRef). - Reply(http.StatusNotFound) - // Run test - version, err := GetDatabaseVersion(context.Background(), projectRef) - // Check error - assert.ErrorContains(t, err, "unexpected retrieve project status 404:") - assert.Empty(t, version) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("handles empty database version", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - projectRef := apitest.RandomProjectRef() - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + projectRef). - Reply(http.StatusOK). - JSON(api.V1ProjectWithDatabaseResponseOutput{}) - // Run test - version, err := GetDatabaseVersion(context.Background(), projectRef) - // Check error - assert.ErrorIs(t, err, errDatabaseVersion) - assert.Empty(t, version) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} diff --git a/apps/cli-go/internal/utils/tenant/gotrue.go b/apps/cli-go/internal/utils/tenant/gotrue.go deleted file mode 100644 index 507a63af5f..0000000000 --- a/apps/cli-go/internal/utils/tenant/gotrue.go +++ /dev/null @@ -1,32 +0,0 @@ -package tenant - -import ( - "context" - "net/http" - - "github.com/go-errors/errors" - "github.com/supabase/cli/pkg/fetcher" -) - -var errGotrueVersion = errors.New("GoTrue version not found.") - -type HealthResponse struct { - Version string `json:"version"` - Name string `json:"name"` - Description string `json:"description"` -} - -func (t *TenantAPI) GetGotrueVersion(ctx context.Context) (string, error) { - resp, err := t.Send(ctx, http.MethodGet, "/auth/v1/health", nil) - if err != nil { - return "", err - } - data, err := fetcher.ParseJSON[HealthResponse](resp.Body) - if err != nil { - return "", err - } - if len(data.Version) == 0 { - return "", errors.New(errGotrueVersion) - } - return data.Version, nil -} diff --git a/apps/cli-go/internal/utils/tenant/gotrue_test.go b/apps/cli-go/internal/utils/tenant/gotrue_test.go deleted file mode 100644 index fcc646e0d7..0000000000 --- a/apps/cli-go/internal/utils/tenant/gotrue_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package tenant - -import ( - "context" - "errors" - "net/http" - "testing" - - "github.com/h2non/gock" - "github.com/stretchr/testify/assert" - "github.com/supabase/cli/pkg/fetcher" -) - -var mockApi = TenantAPI{Fetcher: fetcher.NewFetcher( - "http://127.0.0.1", -)} - -func TestGotrueVersion(t *testing.T) { - t.Run("gets gotrue version", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New("http://127.0.0.1"). - Get("/auth/v1/health"). - Reply(http.StatusOK). - JSON(HealthResponse{Version: "v2.92.1"}) - // Run test - version, err := mockApi.GetGotrueVersion(context.Background()) - // Check error - assert.NoError(t, err) - assert.Equal(t, "v2.92.1", version) - }) - - t.Run("throws error on network error", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New("http://127.0.0.1"). - Get("/auth/v1/health"). - ReplyError(errors.New("network error")) - // Run test - version, err := mockApi.GetGotrueVersion(context.Background()) - // Check error - assert.ErrorContains(t, err, "network error") - assert.Empty(t, version) - }) - - t.Run("throws error on missing version", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New("http://127.0.0.1"). - Get("/auth/v1/health"). - Reply(http.StatusOK). - JSON(HealthResponse{}) - // Run test - version, err := mockApi.GetGotrueVersion(context.Background()) - // Check error - assert.ErrorIs(t, err, errGotrueVersion) - assert.Empty(t, version) - }) -} diff --git a/apps/cli-go/internal/utils/tenant/postgrest.go b/apps/cli-go/internal/utils/tenant/postgrest.go deleted file mode 100644 index cb8c96d28b..0000000000 --- a/apps/cli-go/internal/utils/tenant/postgrest.go +++ /dev/null @@ -1,39 +0,0 @@ -package tenant - -import ( - "context" - "net/http" - "strings" - - "github.com/go-errors/errors" - "github.com/supabase/cli/pkg/fetcher" -) - -var errPostgrestVersion = errors.New("PostgREST version not found.") - -type SwaggerInfo struct { - Title string `json:"title"` - Description string `json:"description"` - Version string `json:"version"` -} - -type SwaggerResponse struct { - Swagger string `json:"swagger"` - Info SwaggerInfo `json:"info"` -} - -func (t *TenantAPI) GetPostgrestVersion(ctx context.Context) (string, error) { - resp, err := t.Send(ctx, http.MethodGet, "/rest/v1/", nil) - if err != nil { - return "", err - } - data, err := fetcher.ParseJSON[SwaggerResponse](resp.Body) - if err != nil { - return "", err - } - if len(data.Info.Version) == 0 { - return "", errors.New(errPostgrestVersion) - } - parts := strings.Fields(data.Info.Version) - return "v" + parts[0], nil -} diff --git a/apps/cli-go/internal/utils/tenant/postgrest_test.go b/apps/cli-go/internal/utils/tenant/postgrest_test.go deleted file mode 100644 index 2be44db441..0000000000 --- a/apps/cli-go/internal/utils/tenant/postgrest_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package tenant - -import ( - "context" - "errors" - "net/http" - "testing" - - "github.com/h2non/gock" - "github.com/stretchr/testify/assert" -) - -func TestPostgrestVersion(t *testing.T) { - t.Run("appends prefix v", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New("http://127.0.0.1"). - Get("/rest/v1/"). - Reply(http.StatusOK). - JSON(SwaggerResponse{Info: SwaggerInfo{Version: "11.1.0"}}) - // Run test - version, err := mockApi.GetPostgrestVersion(context.Background()) - // Check error - assert.NoError(t, err) - assert.Equal(t, "v11.1.0", version) - }) - - t.Run("ignores commit hash", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New("http://127.0.0.1"). - Get("/rest/v1/"). - Reply(http.StatusOK). - JSON(SwaggerResponse{Info: SwaggerInfo{Version: "11.2.0 (c820efb)"}}) - // Run test - version, err := mockApi.GetPostgrestVersion(context.Background()) - // Check error - assert.NoError(t, err) - assert.Equal(t, "v11.2.0", version) - }) - - t.Run("throws error on network error", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New("http://127.0.0.1"). - Get("/rest/v1/"). - ReplyError(errors.New("network error")) - // Run test - version, err := mockApi.GetPostgrestVersion(context.Background()) - // Check error - assert.ErrorContains(t, err, "network error") - assert.Empty(t, version) - }) - - t.Run("throws error on missing version", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New("http://127.0.0.1"). - Get("/rest/v1/"). - Reply(http.StatusOK). - JSON(SwaggerResponse{}) - // Run test - version, err := mockApi.GetPostgrestVersion(context.Background()) - // Check error - assert.ErrorIs(t, err, errPostgrestVersion) - assert.Empty(t, version) - }) -} diff --git a/apps/cli-go/internal/utils/tenant/storage.go b/apps/cli-go/internal/utils/tenant/storage.go deleted file mode 100644 index 02d1490fc4..0000000000 --- a/apps/cli-go/internal/utils/tenant/storage.go +++ /dev/null @@ -1,27 +0,0 @@ -package tenant - -import ( - "context" - "io" - "net/http" - - "github.com/go-errors/errors" -) - -var errStorageVersion = errors.New("Storage version not found.") - -func (t *TenantAPI) GetStorageVersion(ctx context.Context) (string, error) { - resp, err := t.Send(ctx, http.MethodGet, "/storage/v1/version", nil) - if err != nil { - return "", err - } - defer resp.Body.Close() - data, err := io.ReadAll(resp.Body) - if err != nil { - return "", errors.Errorf("failed to read response body: %w", err) - } - if len(data) == 0 || string(data) == "0.0.0" { - return "", errors.New(errStorageVersion) - } - return "v" + string(data), nil -} diff --git a/apps/cli-go/internal/utils/tenant/storage_test.go b/apps/cli-go/internal/utils/tenant/storage_test.go deleted file mode 100644 index f3c0545b37..0000000000 --- a/apps/cli-go/internal/utils/tenant/storage_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package tenant - -import ( - "context" - "errors" - "net/http" - "testing" - - "github.com/h2non/gock" - "github.com/stretchr/testify/assert" -) - -func TestStorageVersion(t *testing.T) { - t.Run("appends prefix v", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New("http://127.0.0.1"). - Get("/storage/v1/version"). - Reply(http.StatusOK). - BodyString("0.40.4") - // Run test - version, err := mockApi.GetStorageVersion(context.Background()) - // Check error - assert.NoError(t, err) - assert.Equal(t, "v0.40.4", version) - }) - - t.Run("throws error on network error", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New("http://127.0.0.1"). - Get("/storage/v1/version"). - ReplyError(errors.New("network error")) - // Run test - version, err := mockApi.GetStorageVersion(context.Background()) - // Check error - assert.ErrorContains(t, err, "network error") - assert.Empty(t, version) - }) - - t.Run("throws error on missing version", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New("http://127.0.0.1"). - Get("/storage/v1/version"). - Reply(http.StatusOK). - BodyString("0.0.0") - // Run test - version, err := mockApi.GetStorageVersion(context.Background()) - // Check error - assert.ErrorIs(t, err, errStorageVersion) - assert.Empty(t, version) - }) -} diff --git a/apps/cli-go/internal/utils/testdata/invalid.json b/apps/cli-go/internal/utils/testdata/invalid.json deleted file mode 100644 index 4182e56951..0000000000 --- a/apps/cli-go/internal/utils/testdata/invalid.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "supabase", - "api_url": "api.supabase.com", - "dashboard_url": "https://supabase.com/dashboard", - "project_host": "https://supabase.co" -} diff --git a/apps/cli-go/internal/utils/testdata/malformed.json b/apps/cli-go/internal/utils/testdata/malformed.json deleted file mode 100644 index 55a4e3c2a8..0000000000 --- a/apps/cli-go/internal/utils/testdata/malformed.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "supabase", - "test_url": "https://api.supabase.com" -} diff --git a/apps/cli-go/internal/utils/testdata/profile.json b/apps/cli-go/internal/utils/testdata/profile.json deleted file mode 100644 index 66ba4ae450..0000000000 --- a/apps/cli-go/internal/utils/testdata/profile.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "supabase", - "api_url": "https://api.supabase.com", - "dashboard_url": "https://supabase.com/dashboard", - "project_host": "supabase.co" -} diff --git a/apps/cli-go/legacy/branch/create/create.go b/apps/cli-go/legacy/branch/create/create.go deleted file mode 100644 index 5501283763..0000000000 --- a/apps/cli-go/legacy/branch/create/create.go +++ /dev/null @@ -1,103 +0,0 @@ -package create - -import ( - "bytes" - "context" - _ "embed" - "fmt" - "io" - "os" - "path/filepath" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/pkg/stdcopy" - "github.com/go-errors/errors" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/internal/utils/flags" -) - -var ( - //go:embed templates/clone.sh - cloneScript string -) - -func Run(branch string, fsys afero.Fs) error { - if err := flags.LoadConfig(fsys); err != nil { - return err - } - if err := utils.AssertSupabaseDbIsRunning(); err != nil { - return err - } - - branchPath := filepath.Join(filepath.Dir(utils.CurrBranchPath), branch) - if err := assertNewBranchIsValid(branchPath, fsys); err != nil { - return nil - } - - var ctx = context.Background() - if err := createBranch(ctx, branch); err != nil { - return err - } - - if err := fsys.MkdirAll(branchPath, 0755); err != nil { - return err - } - - fmt.Println("Created branch " + utils.Aqua(branch) + ".") - return nil -} - -func assertNewBranchIsValid(branchPath string, fsys afero.Fs) error { - branch := filepath.Base(branchPath) - - if utils.IsBranchNameReserved(branch) { - return errors.New("Cannot create branch " + utils.Aqua(branch) + ": branch name is reserved.") - } - - if !utils.BranchNamePattern.MatchString(branch) { - return errors.New("Branch name " + utils.Aqua(branch) + " is invalid. Must match [0-9A-Za-z_-]+.") - } - - if _, err := afero.ReadDir(fsys, branchPath); errors.Is(err, os.ErrNotExist) { - // skip - } else if err != nil { - return err - } else { - return errors.New("Branch " + utils.Aqua(branch) + " already exists.") - } - - return nil -} - -func createBranch(ctx context.Context, branch string) error { - exec, err := utils.Docker.ContainerExecCreate(ctx, utils.DbId, container.ExecOptions{ - Cmd: []string{"/bin/bash", "-c", cloneScript}, - Env: []string{"DB_NAME=" + branch}, - AttachStderr: true, - AttachStdout: true, - }) - if err != nil { - return err - } - // Read exec output - resp, err := utils.Docker.ContainerExecAttach(ctx, exec.ID, container.ExecStartOptions{}) - if err != nil { - return err - } - defer resp.Close() - // Capture error details - var errBuf bytes.Buffer - if _, err := stdcopy.StdCopy(io.Discard, &errBuf, resp.Reader); err != nil { - return err - } - // Get the exit code - iresp, err := utils.Docker.ContainerExecInspect(ctx, exec.ID) - if err != nil { - return err - } - if iresp.ExitCode > 0 { - return errors.New("Error creating branch: " + errBuf.String()) - } - return nil -} diff --git a/apps/cli-go/legacy/branch/create/create_test.go b/apps/cli-go/legacy/branch/create/create_test.go deleted file mode 100644 index 66b919ecbd..0000000000 --- a/apps/cli-go/legacy/branch/create/create_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package create - -import ( - "context" - "net/http" - "testing" - - "github.com/docker/docker/api/types/container" - "github.com/h2non/gock" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/testing/apitest" - "github.com/supabase/cli/internal/utils" -) - -func TestBranchValidation(t *testing.T) { - t.Run("branch name is valid", func(t *testing.T) { - assert.NoError(t, assertNewBranchIsValid("test-branch", afero.NewMemMapFs())) - }) - - t.Run("branch name is reserved", func(t *testing.T) { - assert.Error(t, assertNewBranchIsValid("main", afero.NewMemMapFs())) - }) - - t.Run("branch name is invalid", func(t *testing.T) { - assert.Error(t, assertNewBranchIsValid("@", afero.NewMemMapFs())) - }) - - t.Run("branch not a directory", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - path := "/supabase/.branches/test-branch" - _, err := fsys.Create(path) - require.NoError(t, err) - // Run test - assert.Error(t, assertNewBranchIsValid(path, fsys)) - }) - - t.Run("branch already exists", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - path := "/supabase/.branches/test-branch" - require.NoError(t, fsys.MkdirAll(path, 0755)) - // Run test - assert.Error(t, assertNewBranchIsValid(path, fsys)) - }) -} - -func TestBranchCreation(t *testing.T) { - utils.DbId = "test-db" - - t.Run("docker exec failure", func(t *testing.T) { - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/exec"). - Reply(http.StatusServiceUnavailable) - // Run test - err := createBranch(context.Background(), "test-branch") - // Validate api - assert.ErrorContains(t, err, "request returned 503 Service Unavailable for API route and version") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("docker attach failure", func(t *testing.T) { - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/exec"). - Reply(http.StatusCreated). - JSON(container.InspectResponse{}) - // Run test - err := createBranch(context.Background(), "test-branch") - // Validate api - assert.ErrorContains(t, err, "unable to upgrade to tcp, received 404") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} - -func TestCreateCommand(t *testing.T) { - const branch = "test-branch" - - t.Run("throws error on missing config", func(t *testing.T) { - assert.Error(t, Run(branch, afero.NewMemMapFs())) - }) - - t.Run("throws error on stopped db", func(t *testing.T) { - // Setup in-memory fs - fsys := &afero.MemMapFs{} - require.NoError(t, utils.WriteConfig(fsys, false)) - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers"). - Reply(http.StatusNotFound) - // Run test - err := Run(branch, fsys) - // Validate api - assert.ErrorIs(t, err, utils.ErrNotRunning) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} diff --git a/apps/cli-go/legacy/branch/create/templates/clone.sh b/apps/cli-go/legacy/branch/create/templates/clone.sh deleted file mode 100755 index ac8d6ccaa2..0000000000 --- a/apps/cli-go/legacy/branch/create/templates/clone.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -set -euo pipefail - -createdb --username postgres --host 127.0.0.1 "$DB_NAME" -pg_dump --username postgres --host 127.0.0.1 postgres | psql --username postgres --host 127.0.0.1 "$DB_NAME" diff --git a/apps/cli-go/legacy/branch/delete/delete.go b/apps/cli-go/legacy/branch/delete/delete.go deleted file mode 100644 index ea14cd34d6..0000000000 --- a/apps/cli-go/legacy/branch/delete/delete.go +++ /dev/null @@ -1,90 +0,0 @@ -package delete - -import ( - "bytes" - "context" - "fmt" - "io" - "path/filepath" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/pkg/stdcopy" - "github.com/go-errors/errors" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/internal/utils/flags" -) - -func Run(branch string, fsys afero.Fs) error { - if err := flags.LoadConfig(fsys); err != nil { - return err - } - if err := utils.AssertSupabaseDbIsRunning(); err != nil { - return err - } - - // TODO: update branch history atomically with database - if err := deleteBranchDir(branch, fsys); err != nil { - return err - } - - ctx := context.Background() - if err := deleteBranchPG(ctx, branch); err != nil { - return err - } - - fmt.Println("Deleted branch " + utils.Aqua(branch) + ".") - return nil -} - -func deleteBranchDir(branch string, fsys afero.Fs) error { - if currBranch, _ := utils.GetCurrentBranchFS(fsys); branch == currBranch { - return errors.New("Cannot delete current branch.") - } - - if utils.IsBranchNameReserved(branch) { - return errors.New("Cannot delete branch " + utils.Aqua(branch) + ": branch name is reserved.") - } - - branchPath := filepath.Join(filepath.Dir(utils.CurrBranchPath), branch) - if _, err := afero.ReadDir(fsys, branchPath); err != nil { - return errors.New("Branch " + utils.Aqua(branch) + " does not exist.") - } - - if err := fsys.RemoveAll(branchPath); err != nil { - return errors.Errorf("Failed deleting branch %s: %w", utils.Aqua(branch), err) - } - - return nil -} - -func deleteBranchPG(ctx context.Context, branch string) error { - exec, err := utils.Docker.ContainerExecCreate(ctx, utils.DbId, container.ExecOptions{ - Cmd: []string{"dropdb", "--username", "postgres", "--host", "127.0.0.1", branch}, - AttachStderr: true, - AttachStdout: true, - }) - if err != nil { - return err - } - // Read exec output - resp, err := utils.Docker.ContainerExecAttach(ctx, exec.ID, container.ExecStartOptions{}) - if err != nil { - return err - } - defer resp.Close() - // Capture error details - var errBuf bytes.Buffer - if _, err := stdcopy.StdCopy(io.Discard, &errBuf, resp.Reader); err != nil { - return err - } - // Get the exit code - iresp, err := utils.Docker.ContainerExecInspect(ctx, exec.ID) - if err != nil { - return err - } - if iresp.ExitCode > 0 { - return errors.New("Error deleting branch: " + errBuf.String()) - } - return nil -} diff --git a/apps/cli-go/legacy/branch/delete/delete_test.go b/apps/cli-go/legacy/branch/delete/delete_test.go deleted file mode 100644 index 76957e2460..0000000000 --- a/apps/cli-go/legacy/branch/delete/delete_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package delete - -import ( - "net/http" - "path/filepath" - "testing" - - "github.com/h2non/gock" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/testing/apitest" - "github.com/supabase/cli/internal/utils" -) - -func TestBranchDir(t *testing.T) { - t.Run("removes a branch directory", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - path := filepath.Join(filepath.Dir(utils.CurrBranchPath), "test-branch") - require.NoError(t, fsys.Mkdir(path, 0755)) - // Run test - assert.NoError(t, deleteBranchDir("test-branch", fsys)) - // Validate removal - exists, err := afero.Exists(fsys, path) - assert.NoError(t, err) - assert.False(t, exists) - }) - - t.Run("branch is current", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, afero.WriteFile(fsys, utils.CurrBranchPath, []byte("main"), 0644)) - // Run test - assert.Error(t, deleteBranchDir("main", fsys)) - }) - - t.Run("branch is reserved", func(t *testing.T) { - assert.Error(t, deleteBranchDir("main", afero.NewMemMapFs())) - }) - - t.Run("branch does not exist", func(t *testing.T) { - assert.Error(t, deleteBranchDir("test-branch", afero.NewMemMapFs())) - }) - - t.Run("branch permission denied", func(t *testing.T) { - // Setup read-only fs - fsys := afero.NewMemMapFs() - path := filepath.Join(filepath.Dir(utils.CurrBranchPath), "test-branch") - require.NoError(t, fsys.Mkdir(path, 0755)) - // Run test - assert.Error(t, deleteBranchDir("test-branch", afero.NewReadOnlyFs(fsys))) - }) -} - -func TestDeleteCommand(t *testing.T) { - const branch = "test-branch" - - t.Run("throws error on missing config", func(t *testing.T) { - assert.Error(t, Run(branch, afero.NewMemMapFs())) - }) - - t.Run("throws error on stopped db", func(t *testing.T) { - // Setup in-memory fs - fsys := &afero.MemMapFs{} - require.NoError(t, utils.WriteConfig(fsys, false)) - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers"). - Reply(http.StatusNotFound) - // Run test - err := Run(branch, fsys) - // Validate api - assert.ErrorIs(t, err, utils.ErrNotRunning) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} diff --git a/apps/cli-go/legacy/branch/list/list.go b/apps/cli-go/legacy/branch/list/list.go deleted file mode 100644 index b1037ac1b7..0000000000 --- a/apps/cli-go/legacy/branch/list/list.go +++ /dev/null @@ -1,36 +0,0 @@ -package list - -import ( - "fmt" - "io" - "os" - "path/filepath" - - "github.com/go-errors/errors" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/utils" -) - -func Run(fsys afero.Fs, out io.Writer) error { - branches, err := afero.ReadDir(fsys, filepath.Dir(utils.CurrBranchPath)) - if errors.Is(err, os.ErrNotExist) { - return nil - } else if err != nil { - return err - } - - currBranch, _ := utils.GetCurrentBranchFS(fsys) - for _, branch := range branches { - if branch.Name() == filepath.Base(utils.CurrBranchPath) { - continue - } - - if branch.Name() == currBranch { - fmt.Fprintln(out, "*", branch.Name()) - } else { - fmt.Fprintln(out, " ", branch.Name()) - } - } - - return nil -} diff --git a/apps/cli-go/legacy/branch/list/list_test.go b/apps/cli-go/legacy/branch/list/list_test.go deleted file mode 100644 index 24e34d5e53..0000000000 --- a/apps/cli-go/legacy/branch/list/list_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package list - -import ( - "bytes" - "io" - "path/filepath" - "strings" - "testing" - - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/utils" -) - -func TestListCommand(t *testing.T) { - t.Run("lists all branches", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, afero.WriteFile(fsys, utils.CurrBranchPath, []byte("main"), 0644)) - base := filepath.Dir(utils.CurrBranchPath) - require.NoError(t, fsys.Mkdir(filepath.Join(base, "main"), 0755)) - require.NoError(t, fsys.Mkdir(filepath.Join(base, "test"), 0755)) - // Run test - var out bytes.Buffer - require.NoError(t, Run(fsys, &out)) - // Validate output - lines := strings.Split(out.String(), "\n") - assert.ElementsMatch(t, []string{ - "* main", - " test", - "", - }, lines) - }) - - t.Run("lists without current branch", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - base := filepath.Dir(utils.CurrBranchPath) - require.NoError(t, fsys.Mkdir(filepath.Join(base, "main"), 0755)) - require.NoError(t, fsys.Mkdir(filepath.Join(base, "test"), 0755)) - // Run test - var out bytes.Buffer - require.NoError(t, Run(fsys, &out)) - // Validate output - lines := strings.Split(out.String(), "\n") - assert.ElementsMatch(t, []string{ - " main", - " test", - "", - }, lines) - }) - - t.Run("lists uninitialized branch", func(t *testing.T) { - require.NoError(t, Run(afero.NewMemMapFs(), io.Discard)) - }) - - t.Run("throws error on unreadable directory", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - _, err := fsys.Create(filepath.Dir(utils.CurrBranchPath)) - require.NoError(t, err) - // Run test - require.Error(t, Run(fsys, io.Discard)) - }) -} diff --git a/apps/cli-go/legacy/branch/switch_/switch_.go b/apps/cli-go/legacy/branch/switch_/switch_.go deleted file mode 100644 index d11803e884..0000000000 --- a/apps/cli-go/legacy/branch/switch_/switch_.go +++ /dev/null @@ -1,88 +0,0 @@ -package switch_ - -import ( - "context" - "fmt" - "os" - "path/filepath" - - "github.com/go-errors/errors" - "github.com/jackc/pgconn" - "github.com/jackc/pgx/v4" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/db/reset" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/internal/utils/flags" -) - -func Run(ctx context.Context, target string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { - // 1. Sanity checks - { - if err := flags.LoadConfig(fsys); err != nil { - return err - } - if err := utils.AssertSupabaseDbIsRunning(); err != nil { - return err - } - if target != "main" && utils.IsBranchNameReserved(target) { - return errors.New("Cannot switch branch " + utils.Aqua(target) + ": branch name is reserved.") - } - branchPath := filepath.Join(filepath.Dir(utils.CurrBranchPath), target) - if _, err := fsys.Stat(branchPath); errors.Is(err, os.ErrNotExist) { - return errors.New("Branch " + utils.Aqua(target) + " does not exist.") - } else if err != nil { - return err - } - } - - // 2. Check current branch - currBranch, err := utils.GetCurrentBranchFS(fsys) - if err != nil { - // Assume we are on main branch - currBranch = "main" - } - - // 3. Switch Postgres database - if currBranch == target { - fmt.Println("Already on branch " + utils.Aqua(target) + ".") - } else if err := switchDatabase(ctx, currBranch, target, options...); err != nil { - return errors.New("Error switching to branch " + utils.Aqua(target) + ": " + err.Error()) - } else { - fmt.Println("Switched to branch " + utils.Aqua(target) + ".") - } - - // 4. Update current branch - if err := afero.WriteFile(fsys, utils.CurrBranchPath, []byte(target), 0644); err != nil { - return errors.New("Unable to update local branch file. Fix by running: echo '" + target + "' > " + utils.CurrBranchPath) - } - return nil -} - -func switchDatabase(ctx context.Context, source, target string, options ...func(*pgx.ConnConfig)) error { - conn, err := utils.ConnectLocalPostgres(ctx, pgconn.Config{Database: "template1"}, options...) - if err != nil { - return err - } - defer conn.Close(context.Background()) - if err := reset.DisconnectClients(ctx, conn); err != nil { - return err - } - defer func() { - if err := reset.RestartDatabase(context.Background(), os.Stderr); err != nil { - fmt.Fprintln(os.Stderr, "Failed to restart database:", err) - } - }() - backup := "ALTER DATABASE postgres RENAME TO " + source + ";" - if _, err := conn.Exec(ctx, backup); err != nil { - return err - } - rename := "ALTER DATABASE " + target + " RENAME TO postgres;" - if _, err := conn.Exec(ctx, rename); err != nil { - rollback := "ALTER DATABASE " + source + " RENAME TO postgres;" - if _, err := conn.Exec(ctx, rollback); err != nil { - fmt.Fprintln(os.Stderr, "Failed to rollback database:", err) - } - return err - } - return nil -} diff --git a/apps/cli-go/legacy/branch/switch_/switch__test.go b/apps/cli-go/legacy/branch/switch_/switch__test.go deleted file mode 100644 index f0e51d521e..0000000000 --- a/apps/cli-go/legacy/branch/switch_/switch__test.go +++ /dev/null @@ -1,290 +0,0 @@ -package switch_ - -import ( - "context" - "net/http" - "path/filepath" - "testing" - - "github.com/docker/docker/api/types/container" - "github.com/h2non/gock" - "github.com/jackc/pgerrcode" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/db/reset" - "github.com/supabase/cli/internal/testing/apitest" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/pgtest" -) - -func TestSwitchCommand(t *testing.T) { - t.Run("switches local branch", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - // Setup target branch - branch := "target" - branchPath := filepath.Join(filepath.Dir(utils.CurrBranchPath), branch) - require.NoError(t, fsys.Mkdir(branchPath, 0755)) - require.NoError(t, afero.WriteFile(fsys, utils.CurrBranchPath, []byte("main"), 0644)) - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers"). - Reply(http.StatusOK). - JSON(container.InspectResponse{}) - gock.New(utils.Docker.DaemonHost()). - Post("/v" + utils.Docker.ClientVersion() + "/containers"). - Reply(http.StatusServiceUnavailable) - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query("ALTER DATABASE postgres ALLOW_CONNECTIONS false"). - Reply("ALTER DATABASE"). - Query("ALTER DATABASE _supabase ALLOW_CONNECTIONS false"). - Reply("ALTER DATABASE"). - Query(reset.TERMINATE_BACKENDS). - Reply("SELECT 1"). - Query(reset.COUNT_REPLICATION_SLOTS). - Reply("SELECT 1", []any{0}). - Query("ALTER DATABASE postgres RENAME TO main;"). - Reply("ALTER DATABASE"). - Query("ALTER DATABASE " + branch + " RENAME TO postgres;"). - Reply("ALTER DATABASE") - // Run test - assert.NoError(t, Run(context.Background(), branch, fsys, conn.Intercept)) - // Validate output - assert.Empty(t, apitest.ListUnmatchedRequests()) - contents, err := afero.ReadFile(fsys, utils.CurrBranchPath) - assert.NoError(t, err) - assert.Equal(t, []byte(branch), contents) - }) - - t.Run("throws error on malformed config", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, afero.WriteFile(fsys, utils.ConfigPath, []byte("malformed"), 0644)) - // Run test - err := Run(context.Background(), "target", fsys) - // Check error - assert.ErrorContains(t, err, "toml: expected = after a key, but the document ends there") - }) - - t.Run("throws error on missing database", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers"). - Reply(http.StatusNotFound) - // Run test - err := Run(context.Background(), "target", fsys) - // Check error - assert.ErrorIs(t, err, utils.ErrNotRunning) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on reserved branch", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers"). - Reply(http.StatusOK). - JSON(container.InspectResponse{}) - // Run test - err := Run(context.Background(), "postgres", fsys) - // Check error - assert.ErrorContains(t, err, "branch name is reserved.") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on missing branch", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers"). - Reply(http.StatusOK). - JSON(container.InspectResponse{}) - // Run test - err := Run(context.Background(), "main", fsys) - // Check error - assert.ErrorContains(t, err, "Branch main does not exist.") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("noop on current branch", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers"). - Reply(http.StatusOK). - JSON(container.InspectResponse{}) - // Setup target branch - branch := "main" - branchPath := filepath.Join(filepath.Dir(utils.CurrBranchPath), branch) - require.NoError(t, fsys.Mkdir(branchPath, 0755)) - // Run test - assert.NoError(t, Run(context.Background(), branch, fsys)) - // Check error - assert.Empty(t, apitest.ListUnmatchedRequests()) - contents, err := afero.ReadFile(fsys, utils.CurrBranchPath) - assert.NoError(t, err) - assert.Equal(t, []byte(branch), contents) - }) - - t.Run("throws error on failure to switch", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers"). - Reply(http.StatusOK). - JSON(container.InspectResponse{}) - // Setup target branch - branch := "target" - branchPath := filepath.Join(filepath.Dir(utils.CurrBranchPath), branch) - require.NoError(t, fsys.Mkdir(branchPath, 0755)) - // Setup mock postgres - conn := pgtest.NewConn() - // Run test - err := Run(context.Background(), branch, fsys, conn.Intercept) - // Check error - assert.ErrorContains(t, err, "Error switching to branch target") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on failure to write", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Get("/v" + utils.Docker.ClientVersion() + "/containers"). - Reply(http.StatusOK). - JSON(container.InspectResponse{}) - // Setup target branch - branch := "main" - branchPath := filepath.Join(filepath.Dir(utils.CurrBranchPath), branch) - require.NoError(t, fsys.Mkdir(branchPath, 0755)) - // Run test - err := Run(context.Background(), branch, afero.NewReadOnlyFs(fsys)) - // Check error - assert.ErrorContains(t, err, "Unable to update local branch file.") - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} - -func TestSwitchDatabase(t *testing.T) { - t.Run("throws error on failure to connect", func(t *testing.T) { - // Setup invalid port - utils.Config.Db.Port = 0 - // Run test - err := switchDatabase(context.Background(), "main", "target") - // Check error - assert.ErrorContains(t, err, "invalid port") - }) - - t.Run("throws error on failure to disconnect", func(t *testing.T) { - // Setup valid config - utils.Config.Db.Port = 54322 - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query("ALTER DATABASE postgres ALLOW_CONNECTIONS false"). - ReplyError(pgerrcode.InvalidParameterValue, `cannot disallow connections for current database`). - Query("ALTER DATABASE _supabase ALLOW_CONNECTIONS false"). - Query(reset.TERMINATE_BACKENDS) - // Run test - err := switchDatabase(context.Background(), "main", "target", conn.Intercept) - // Check error - assert.ErrorContains(t, err, pgerrcode.InvalidParameterValue) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on failure to backup", func(t *testing.T) { - // Setup valid config - utils.DbId = "test-switch" - utils.Config.Db.Port = 54322 - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query("ALTER DATABASE postgres ALLOW_CONNECTIONS false"). - Reply("ALTER DATABASE"). - Query("ALTER DATABASE _supabase ALLOW_CONNECTIONS false"). - Reply("ALTER DATABASE"). - Query(reset.TERMINATE_BACKENDS). - Reply("SELECT 1"). - Query(reset.COUNT_REPLICATION_SLOTS). - Reply("SELECT 1", []any{0}). - Query("ALTER DATABASE postgres RENAME TO main;"). - ReplyError(pgerrcode.DuplicateDatabase, `database "main" already exists`) - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/restart"). - Reply(http.StatusServiceUnavailable) - // Run test - err := switchDatabase(context.Background(), "main", "target", conn.Intercept) - // Check error - assert.ErrorContains(t, err, pgerrcode.DuplicateDatabase) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on failure to rename", func(t *testing.T) { - // Setup valid config - utils.DbId = "test-switch" - utils.Config.Db.Port = 54322 - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query("ALTER DATABASE postgres ALLOW_CONNECTIONS false"). - Reply("ALTER DATABASE"). - Query("ALTER DATABASE _supabase ALLOW_CONNECTIONS false"). - Reply("ALTER DATABASE"). - Query(reset.TERMINATE_BACKENDS). - Reply("SELECT 1"). - Query(reset.COUNT_REPLICATION_SLOTS). - Reply("SELECT 1", []any{0}). - Query("ALTER DATABASE postgres RENAME TO main;"). - Reply("ALTER DATABASE"). - Query("ALTER DATABASE target RENAME TO postgres;"). - ReplyError(pgerrcode.InvalidCatalogName, `database "target" does not exist`). - // Attempt to rollback - Query("ALTER DATABASE main RENAME TO postgres;"). - ReplyError(pgerrcode.DuplicateDatabase, `database "postgres" already exists`) - // Setup mock docker - require.NoError(t, apitest.MockDocker(utils.Docker)) - defer gock.OffAll() - gock.New(utils.Docker.DaemonHost()). - Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/restart"). - Reply(http.StatusServiceUnavailable) - // Run test - err := switchDatabase(context.Background(), "main", "target", conn.Intercept) - // Check error - assert.ErrorContains(t, err, pgerrcode.InvalidCatalogName) - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) -} diff --git a/apps/cli-go/legacy/keys/keys.go b/apps/cli-go/legacy/keys/keys.go deleted file mode 100644 index 560a4ce917..0000000000 --- a/apps/cli-go/legacy/keys/keys.go +++ /dev/null @@ -1,75 +0,0 @@ -package keys - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "fmt" - "os" - "strings" - - "github.com/go-errors/errors" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/config" -) - -type CustomName struct { - DbHost string `env:"db.host,default=NEXT_PUBLIC_SUPABASE_URL"` - DbPassword string `env:"db.password,default=SUPABASE_DB_PASSWORD"` - JWTSecret string `env:"db.password,default=SUPABASE_AUTH_JWT_SECRET"` - AnonKey string `env:"auth.anon_key,default=SUPABASE_AUTH_ANON_KEY"` - ServiceRoleKey string `env:"auth.service_role_key,default=SUPABASE_AUTH_SERVICE_ROLE_KEY"` -} - -func Run(ctx context.Context, projectRef, format string, names CustomName, fsys afero.Fs) error { - branch := utils.GetGitBranch(fsys) - if err := GenerateSecrets(ctx, projectRef, branch, fsys); err != nil { - return err - } - return utils.EncodeOutput(format, os.Stdout, map[string]string{ - names.DbHost: fmt.Sprintf("%s-%s.fly.dev", projectRef, branch), - names.DbPassword: utils.Config.Db.Password, - names.JWTSecret: utils.Config.Auth.JwtSecret.Value, - names.AnonKey: utils.Config.Auth.AnonKey.Value, - names.ServiceRoleKey: utils.Config.Auth.ServiceRoleKey.Value, - }) -} - -func GenerateSecrets(ctx context.Context, projectRef, branch string, fsys afero.Fs) error { - // Load JWT secret from api - resp, err := utils.GetSupabase().V1GetPostgrestServiceConfigWithResponse(ctx, projectRef) - if err != nil { - return errors.Errorf("failed to get postgrest config: %w", err) - } - if resp.JSON200 == nil { - return errors.New("Unexpected error retrieving JWT secret: " + string(resp.Body)) - } - utils.Config.Auth.JwtSecret.Value = *resp.JSON200.JwtSecret - // Generate database password - key := strings.Join([]string{ - projectRef, - utils.Config.Auth.JwtSecret.Value, - branch, - }, ":") - hash := sha256.Sum256([]byte(key)) - utils.Config.Db.Password = hex.EncodeToString(hash[:]) - // Generate JWT tokens - anonToken := config.CustomClaims{ - Issuer: "supabase", - Ref: projectRef, - Role: "anon", - }.NewToken() - if utils.Config.Auth.AnonKey.Value, err = anonToken.SignedString([]byte(utils.Config.Auth.JwtSecret.Value)); err != nil { - return errors.Errorf("failed to sign anon key: %w", err) - } - serviceToken := config.CustomClaims{ - Issuer: "supabase", - Ref: projectRef, - Role: "service_role", - }.NewToken() - if utils.Config.Auth.ServiceRoleKey.Value, err = serviceToken.SignedString([]byte(utils.Config.Auth.JwtSecret.Value)); err != nil { - return errors.Errorf("failed to sign service_role key: %w", err) - } - return nil -} diff --git a/apps/cli-go/main.go b/apps/cli-go/main.go deleted file mode 100644 index bcc8fa3db1..0000000000 --- a/apps/cli-go/main.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import ( - "github.com/supabase/cli/cmd" -) - -// Codegen reads the committed api/v1-openapi.yaml snapshot instead of fetching -// the live upstream spec, so `go generate` is reproducible offline and the -// Codegen CI check only fails on a PR that leaves pkg/api out of sync with the -// snapshot. The API Sync workflow refreshes the snapshot and pkg/api together. -//go:generate go tool oapi-codegen -config pkg/api/types.cfg.yaml api/v1-openapi.yaml -//go:generate go tool oapi-codegen -config pkg/api/client.cfg.yaml api/v1-openapi.yaml - -func main() { - cmd.Execute() -} diff --git a/apps/cli-go/package.json b/apps/cli-go/package.json deleted file mode 100644 index 197015c913..0000000000 --- a/apps/cli-go/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "@supabase/cli-go", - "private": true, - "scripts": { - "build": "go build -o supabase-go .", - "lint:check": "golangci-lint run --timeout 5m", - "lint:fix": "golangci-lint run --fix", - "test:unit": "pnpm exec turbo run @supabase/cli-go#test:unit:run --", - "test:unit:run": "go test ./..." - } -} diff --git a/apps/cli-go/pkg/api/client.cfg.yaml b/apps/cli-go/pkg/api/client.cfg.yaml deleted file mode 100755 index f06e6cad13..0000000000 --- a/apps/cli-go/pkg/api/client.cfg.yaml +++ /dev/null @@ -1,7 +0,0 @@ -package: api -generate: - client: true -output-options: - overlay: - path: api/overlay.yaml -output: pkg/api/client.gen.go diff --git a/apps/cli-go/pkg/api/client.gen.go b/apps/cli-go/pkg/api/client.gen.go deleted file mode 100644 index 98ea157712..0000000000 --- a/apps/cli-go/pkg/api/client.gen.go +++ /dev/null @@ -1,23229 +0,0 @@ -// Package api provides primitives to interact with the openapi HTTP API. -// -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.7.1 DO NOT EDIT. -package api - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "strings" - - "github.com/oapi-codegen/runtime" - openapi_types "github.com/oapi-codegen/runtime/types" -) - -// RequestEditorFn is the function signature for the RequestEditor callback function -type RequestEditorFn func(ctx context.Context, req *http.Request) error - -// Doer performs HTTP requests. -// -// The standard http.Client implements this interface. -type HttpRequestDoer interface { - Do(req *http.Request) (*http.Response, error) -} - -// Client which conforms to the OpenAPI3 specification for this service. -type Client struct { - // The endpoint of the server conforming to this interface, with scheme, - // https://api.deepmap.com for example. This can contain a path relative - // to the server, such as https://api.deepmap.com/dev-test, and all the - // paths in the swagger spec will be appended to the server. - Server string - - // Doer for performing requests, typically a *http.Client with any - // customized settings, such as certificate chains. - Client HttpRequestDoer - - // A list of callbacks for modifying requests which are generated before sending over - // the network. - RequestEditors []RequestEditorFn -} - -// ClientOption allows setting custom parameters during construction -type ClientOption func(*Client) error - -// Creates a new Client, with reasonable defaults -func NewClient(server string, opts ...ClientOption) (*Client, error) { - // create a client with sane default values - client := Client{ - Server: server, - } - // mutate client and add all optional params - for _, o := range opts { - if err := o(&client); err != nil { - return nil, err - } - } - // ensure the server URL always has a trailing slash - if !strings.HasSuffix(client.Server, "/") { - client.Server += "/" - } - // create httpClient, if not already present - if client.Client == nil { - client.Client = &http.Client{} - } - return &client, nil -} - -// WithHTTPClient allows overriding the default Doer, which is -// automatically created using http.Client. This is useful for tests. -func WithHTTPClient(doer HttpRequestDoer) ClientOption { - return func(c *Client) error { - c.Client = doer - return nil - } -} - -// WithRequestEditorFn allows setting up a callback function, which will be -// called right before sending the request. This can be used to mutate the request. -func WithRequestEditorFn(fn RequestEditorFn) ClientOption { - return func(c *Client) error { - c.RequestEditors = append(c.RequestEditors, fn) - return nil - } -} - -// The interface specification for the client above. -type ClientInterface interface { - // V1DeleteABranch request - V1DeleteABranch(ctx context.Context, branchIdOrRef string, params *V1DeleteABranchParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetABranchConfig request - V1GetABranchConfig(ctx context.Context, branchIdOrRef string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdateABranchConfigWithBody request with any body - V1UpdateABranchConfigWithBody(ctx context.Context, branchIdOrRef string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpdateABranchConfig(ctx context.Context, branchIdOrRef string, body V1UpdateABranchConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1DiffABranch request - V1DiffABranch(ctx context.Context, branchIdOrRef string, params *V1DiffABranchParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1MergeABranchWithBody request with any body - V1MergeABranchWithBody(ctx context.Context, branchIdOrRef string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1MergeABranch(ctx context.Context, branchIdOrRef string, body V1MergeABranchJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1PushABranchWithBody request with any body - V1PushABranchWithBody(ctx context.Context, branchIdOrRef string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1PushABranch(ctx context.Context, branchIdOrRef string, body V1PushABranchJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ResetABranchWithBody request with any body - V1ResetABranchWithBody(ctx context.Context, branchIdOrRef string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1ResetABranch(ctx context.Context, branchIdOrRef string, body V1ResetABranchJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1RestoreABranch request - V1RestoreABranch(ctx context.Context, branchIdOrRef string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1AuthorizeUser request - V1AuthorizeUser(ctx context.Context, params *V1AuthorizeUserParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1OauthAuthorizeProjectClaim request - V1OauthAuthorizeProjectClaim(ctx context.Context, params *V1OauthAuthorizeProjectClaimParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1RevokeTokenWithBody request with any body - V1RevokeTokenWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1RevokeToken(ctx context.Context, body V1RevokeTokenJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ExchangeOauthTokenWithBody request with any body - V1ExchangeOauthTokenWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1ExchangeOauthTokenWithFormdataBody(ctx context.Context, body V1ExchangeOauthTokenFormdataRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ListAllOrganizations request - V1ListAllOrganizations(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1CreateAnOrganizationWithBody request with any body - V1CreateAnOrganizationWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1CreateAnOrganization(ctx context.Context, body V1CreateAnOrganizationJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetAnOrganization request - V1GetAnOrganization(ctx context.Context, slug string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetOrganizationEntitlements request - V1GetOrganizationEntitlements(ctx context.Context, slug string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ListOrganizationMembers request - V1ListOrganizationMembers(ctx context.Context, slug string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetOrganizationProjectClaim request - V1GetOrganizationProjectClaim(ctx context.Context, slug string, token string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ClaimProjectForOrganization request - V1ClaimProjectForOrganization(ctx context.Context, slug string, token string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetAllProjectsForOrganization request - V1GetAllProjectsForOrganization(ctx context.Context, slug string, params *V1GetAllProjectsForOrganizationParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetProfile request - V1GetProfile(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ListAllProjects request - V1ListAllProjects(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1CreateAProjectWithBody request with any body - V1CreateAProjectWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1CreateAProject(ctx context.Context, body V1CreateAProjectJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetAvailableRegions request - V1GetAvailableRegions(ctx context.Context, params *V1GetAvailableRegionsParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1DeleteAProject request - V1DeleteAProject(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetProject request - V1GetProject(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdateAProjectWithBody request with any body - V1UpdateAProjectWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpdateAProject(ctx context.Context, ref string, body V1UpdateAProjectJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ListActionRuns request - V1ListActionRuns(ctx context.Context, ref string, params *V1ListActionRunsParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1CountActionRuns request - V1CountActionRuns(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetActionRun request - V1GetActionRun(ctx context.Context, ref string, runId string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetActionRunLogs request - V1GetActionRunLogs(ctx context.Context, ref string, runId string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdateActionRunStatusWithBody request with any body - V1UpdateActionRunStatusWithBody(ctx context.Context, ref string, runId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpdateActionRunStatus(ctx context.Context, ref string, runId string, body V1UpdateActionRunStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetPerformanceAdvisors request - V1GetPerformanceAdvisors(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetSecurityAdvisors request - V1GetSecurityAdvisors(ctx context.Context, ref string, params *V1GetSecurityAdvisorsParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetProjectFunctionCombinedStats request - V1GetProjectFunctionCombinedStats(ctx context.Context, ref string, params *V1GetProjectFunctionCombinedStatsParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetProjectLogs request - V1GetProjectLogs(ctx context.Context, ref string, params *V1GetProjectLogsParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetProjectLogsAll request - V1GetProjectLogsAll(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ScrapeProjectMetrics request - V1ScrapeProjectMetrics(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetProjectUsageApiCount request - V1GetProjectUsageApiCount(ctx context.Context, ref string, params *V1GetProjectUsageApiCountParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetProjectUsageRequestCount request - V1GetProjectUsageRequestCount(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetProjectApiKeys request - V1GetProjectApiKeys(ctx context.Context, ref string, params *V1GetProjectApiKeysParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1CreateProjectApiKeyWithBody request with any body - V1CreateProjectApiKeyWithBody(ctx context.Context, ref string, params *V1CreateProjectApiKeyParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1CreateProjectApiKey(ctx context.Context, ref string, params *V1CreateProjectApiKeyParams, body V1CreateProjectApiKeyJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetProjectLegacyApiKeys request - V1GetProjectLegacyApiKeys(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdateProjectLegacyApiKeys request - V1UpdateProjectLegacyApiKeys(ctx context.Context, ref string, params *V1UpdateProjectLegacyApiKeysParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1DeleteProjectApiKey request - V1DeleteProjectApiKey(ctx context.Context, ref string, id openapi_types.UUID, params *V1DeleteProjectApiKeyParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetProjectApiKey request - V1GetProjectApiKey(ctx context.Context, ref string, id openapi_types.UUID, params *V1GetProjectApiKeyParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdateProjectApiKeyWithBody request with any body - V1UpdateProjectApiKeyWithBody(ctx context.Context, ref string, id openapi_types.UUID, params *V1UpdateProjectApiKeyParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpdateProjectApiKey(ctx context.Context, ref string, id openapi_types.UUID, params *V1UpdateProjectApiKeyParams, body V1UpdateProjectApiKeyJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ListProjectAddons request - V1ListProjectAddons(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ApplyProjectAddonWithBody request with any body - V1ApplyProjectAddonWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1ApplyProjectAddon(ctx context.Context, ref string, body V1ApplyProjectAddonJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1RemoveProjectAddon request - V1RemoveProjectAddon(ctx context.Context, ref string, addonVariant struct { - union json.RawMessage - }, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1DisablePreviewBranching request - V1DisablePreviewBranching(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ListAllBranches request - V1ListAllBranches(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1CreateABranchWithBody request with any body - V1CreateABranchWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1CreateABranch(ctx context.Context, ref string, body V1CreateABranchJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetABranch request - V1GetABranch(ctx context.Context, ref string, name string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1DeleteProjectClaimToken request - V1DeleteProjectClaimToken(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetProjectClaimToken request - V1GetProjectClaimToken(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1CreateProjectClaimToken request - V1CreateProjectClaimToken(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1DeleteLoginRoles request - V1DeleteLoginRoles(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1CreateLoginRoleWithBody request with any body - V1CreateLoginRoleWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1CreateLoginRole(ctx context.Context, ref string, body V1CreateLoginRoleJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetAuthServiceConfig request - V1GetAuthServiceConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdateAuthServiceConfigWithBody request with any body - V1UpdateAuthServiceConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpdateAuthServiceConfig(ctx context.Context, ref string, body V1UpdateAuthServiceConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetProjectSigningKeys request - V1GetProjectSigningKeys(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1CreateProjectSigningKeyWithBody request with any body - V1CreateProjectSigningKeyWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1CreateProjectSigningKey(ctx context.Context, ref string, body V1CreateProjectSigningKeyJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetLegacySigningKey request - V1GetLegacySigningKey(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1CreateLegacySigningKey request - V1CreateLegacySigningKey(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1RemoveProjectSigningKey request - V1RemoveProjectSigningKey(ctx context.Context, ref string, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetProjectSigningKey request - V1GetProjectSigningKey(ctx context.Context, ref string, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdateProjectSigningKeyWithBody request with any body - V1UpdateProjectSigningKeyWithBody(ctx context.Context, ref string, id openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpdateProjectSigningKey(ctx context.Context, ref string, id openapi_types.UUID, body V1UpdateProjectSigningKeyJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ListAllSsoProvider request - V1ListAllSsoProvider(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1CreateASsoProviderWithBody request with any body - V1CreateASsoProviderWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1CreateASsoProvider(ctx context.Context, ref string, body V1CreateASsoProviderJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1DeleteASsoProvider request - V1DeleteASsoProvider(ctx context.Context, ref string, providerId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetASsoProvider request - V1GetASsoProvider(ctx context.Context, ref string, providerId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdateASsoProviderWithBody request with any body - V1UpdateASsoProviderWithBody(ctx context.Context, ref string, providerId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpdateASsoProvider(ctx context.Context, ref string, providerId openapi_types.UUID, body V1UpdateASsoProviderJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ListProjectTpaIntegrations request - V1ListProjectTpaIntegrations(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1CreateProjectTpaIntegrationWithBody request with any body - V1CreateProjectTpaIntegrationWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1CreateProjectTpaIntegration(ctx context.Context, ref string, body V1CreateProjectTpaIntegrationJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1DeleteProjectTpaIntegration request - V1DeleteProjectTpaIntegration(ctx context.Context, ref string, tpaId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetProjectTpaIntegration request - V1GetProjectTpaIntegration(ctx context.Context, ref string, tpaId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetProjectPgbouncerConfig request - V1GetProjectPgbouncerConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetPoolerConfig request - V1GetPoolerConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdatePoolerConfigWithBody request with any body - V1UpdatePoolerConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpdatePoolerConfig(ctx context.Context, ref string, body V1UpdatePoolerConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetPostgresConfig request - V1GetPostgresConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdatePostgresConfigWithBody request with any body - V1UpdatePostgresConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpdatePostgresConfig(ctx context.Context, ref string, body V1UpdatePostgresConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetDatabaseDisk request - V1GetDatabaseDisk(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ModifyDatabaseDiskWithBody request with any body - V1ModifyDatabaseDiskWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1ModifyDatabaseDisk(ctx context.Context, ref string, body V1ModifyDatabaseDiskJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetProjectDiskAutoscaleConfig request - V1GetProjectDiskAutoscaleConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetDiskUtilization request - V1GetDiskUtilization(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetRealtimeConfig request - V1GetRealtimeConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdateRealtimeConfigWithBody request with any body - V1UpdateRealtimeConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpdateRealtimeConfig(ctx context.Context, ref string, body V1UpdateRealtimeConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ShutdownRealtime request - V1ShutdownRealtime(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetStorageConfig request - V1GetStorageConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdateStorageConfigWithBody request with any body - V1UpdateStorageConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpdateStorageConfig(ctx context.Context, ref string, body V1UpdateStorageConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1DeleteHostnameConfig request - V1DeleteHostnameConfig(ctx context.Context, ref string, params *V1DeleteHostnameConfigParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetHostnameConfig request - V1GetHostnameConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ActivateCustomHostname request - V1ActivateCustomHostname(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdateHostnameConfigWithBody request with any body - V1UpdateHostnameConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpdateHostnameConfig(ctx context.Context, ref string, body V1UpdateHostnameConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1VerifyDnsConfig request - V1VerifyDnsConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ListAllBackups request - V1ListAllBackups(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1RestorePhysicalBackupWithBody request with any body - V1RestorePhysicalBackupWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1RestorePhysicalBackup(ctx context.Context, ref string, body V1RestorePhysicalBackupJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1RestorePitrBackupWithBody request with any body - V1RestorePitrBackupWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1RestorePitrBackup(ctx context.Context, ref string, body V1RestorePitrBackupJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetRestorePoint request - V1GetRestorePoint(ctx context.Context, ref string, params *V1GetRestorePointParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1CreateRestorePointWithBody request with any body - V1CreateRestorePointWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1CreateRestorePoint(ctx context.Context, ref string, body V1CreateRestorePointJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetBackupSchedule request - V1GetBackupSchedule(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdateBackupScheduleWithBody request with any body - V1UpdateBackupScheduleWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpdateBackupSchedule(ctx context.Context, ref string, body V1UpdateBackupScheduleJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UndoWithBody request with any body - V1UndoWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1Undo(ctx context.Context, ref string, body V1UndoJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetDatabaseMetadata request - V1GetDatabaseMetadata(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetJitAccess request - V1GetJitAccess(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1AuthorizeJitAccessWithBody request with any body - V1AuthorizeJitAccessWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1AuthorizeJitAccess(ctx context.Context, ref string, body V1AuthorizeJitAccessJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdateJitAccessWithBody request with any body - V1UpdateJitAccessWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpdateJitAccess(ctx context.Context, ref string, body V1UpdateJitAccessJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1InviteExternalJitAccessWithBody request with any body - V1InviteExternalJitAccessWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1InviteExternalJitAccess(ctx context.Context, ref string, body V1InviteExternalJitAccessJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1AcceptInviteExternalJitAccessWithBody request with any body - V1AcceptInviteExternalJitAccessWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1AcceptInviteExternalJitAccess(ctx context.Context, ref string, body V1AcceptInviteExternalJitAccessJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1DeleteInviteExternalJitAccess request - V1DeleteInviteExternalJitAccess(ctx context.Context, ref string, inviteId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ListJitAccess request - V1ListJitAccess(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1DeleteJitAccess request - V1DeleteJitAccess(ctx context.Context, ref string, userId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1RollbackMigrations request - V1RollbackMigrations(ctx context.Context, ref string, params *V1RollbackMigrationsParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ListMigrationHistory request - V1ListMigrationHistory(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ApplyAMigrationWithBody request with any body - V1ApplyAMigrationWithBody(ctx context.Context, ref string, params *V1ApplyAMigrationParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1ApplyAMigration(ctx context.Context, ref string, params *V1ApplyAMigrationParams, body V1ApplyAMigrationJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpsertAMigrationWithBody request with any body - V1UpsertAMigrationWithBody(ctx context.Context, ref string, params *V1UpsertAMigrationParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpsertAMigration(ctx context.Context, ref string, params *V1UpsertAMigrationParams, body V1UpsertAMigrationJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetAMigration request - V1GetAMigration(ctx context.Context, ref string, version string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1PatchAMigrationWithBody request with any body - V1PatchAMigrationWithBody(ctx context.Context, ref string, version string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1PatchAMigration(ctx context.Context, ref string, version string, body V1PatchAMigrationJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetDatabaseOpenapi request - V1GetDatabaseOpenapi(ctx context.Context, ref string, params *V1GetDatabaseOpenapiParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdateDatabasePasswordWithBody request with any body - V1UpdateDatabasePasswordWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpdateDatabasePassword(ctx context.Context, ref string, body V1UpdateDatabasePasswordJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1RunAQueryWithBody request with any body - V1RunAQueryWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1RunAQuery(ctx context.Context, ref string, body V1RunAQueryJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ReadOnlyQueryWithBody request with any body - V1ReadOnlyQueryWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1ReadOnlyQuery(ctx context.Context, ref string, body V1ReadOnlyQueryJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1EnableDatabaseWebhook request - V1EnableDatabaseWebhook(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ListAllFunctions request - V1ListAllFunctions(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1CreateAFunctionWithBody request with any body - V1CreateAFunctionWithBody(ctx context.Context, ref string, params *V1CreateAFunctionParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1CreateAFunction(ctx context.Context, ref string, params *V1CreateAFunctionParams, body V1CreateAFunctionJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1BulkUpdateFunctionsWithBody request with any body - V1BulkUpdateFunctionsWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1BulkUpdateFunctions(ctx context.Context, ref string, body V1BulkUpdateFunctionsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1DeployAFunctionWithBody request with any body - V1DeployAFunctionWithBody(ctx context.Context, ref string, params *V1DeployAFunctionParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1DeleteAFunction request - V1DeleteAFunction(ctx context.Context, ref string, functionSlug string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetAFunction request - V1GetAFunction(ctx context.Context, ref string, functionSlug string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdateAFunctionWithBody request with any body - V1UpdateAFunctionWithBody(ctx context.Context, ref string, functionSlug string, params *V1UpdateAFunctionParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpdateAFunction(ctx context.Context, ref string, functionSlug string, params *V1UpdateAFunctionParams, body V1UpdateAFunctionJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetAFunctionBody request - V1GetAFunctionBody(ctx context.Context, ref string, functionSlug string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetServicesHealth request - V1GetServicesHealth(ctx context.Context, ref string, params *V1GetServicesHealthParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetJitAccessConfig request - V1GetJitAccessConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdateJitAccessConfigWithBody request with any body - V1UpdateJitAccessConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpdateJitAccessConfig(ctx context.Context, ref string, body V1UpdateJitAccessConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1DeleteNetworkBansWithBody request with any body - V1DeleteNetworkBansWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1DeleteNetworkBans(ctx context.Context, ref string, body V1DeleteNetworkBansJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ListAllNetworkBans request - V1ListAllNetworkBans(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ListAllNetworkBansEnriched request - V1ListAllNetworkBansEnriched(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetNetworkRestrictions request - V1GetNetworkRestrictions(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1PatchNetworkRestrictionsWithBody request with any body - V1PatchNetworkRestrictionsWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1PatchNetworkRestrictions(ctx context.Context, ref string, body V1PatchNetworkRestrictionsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdateNetworkRestrictionsWithBody request with any body - V1UpdateNetworkRestrictionsWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpdateNetworkRestrictions(ctx context.Context, ref string, body V1UpdateNetworkRestrictionsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1PauseAProject request - V1PauseAProject(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetPgsodiumConfig request - V1GetPgsodiumConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdatePgsodiumConfigWithBody request with any body - V1UpdatePgsodiumConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpdatePgsodiumConfig(ctx context.Context, ref string, body V1UpdatePgsodiumConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetPostgrestServiceConfig request - V1GetPostgrestServiceConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdatePostgrestServiceConfigWithBody request with any body - V1UpdatePostgrestServiceConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpdatePostgrestServiceConfig(ctx context.Context, ref string, body V1UpdatePostgrestServiceConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1RemoveAReadReplicaWithBody request with any body - V1RemoveAReadReplicaWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1RemoveAReadReplica(ctx context.Context, ref string, body V1RemoveAReadReplicaJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1SetupAReadReplicaWithBody request with any body - V1SetupAReadReplicaWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1SetupAReadReplica(ctx context.Context, ref string, body V1SetupAReadReplicaJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetReadonlyModeStatus request - V1GetReadonlyModeStatus(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1DisableReadonlyModeTemporarily request - V1DisableReadonlyModeTemporarily(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1RestartAProject request - V1RestartAProject(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ListAvailableRestoreVersions request - V1ListAvailableRestoreVersions(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1RestoreAProject request - V1RestoreAProject(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1CancelAProjectRestoration request - V1CancelAProjectRestoration(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1BulkDeleteSecretsWithBody request with any body - V1BulkDeleteSecretsWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1BulkDeleteSecrets(ctx context.Context, ref string, body V1BulkDeleteSecretsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ListAllSecrets request - V1ListAllSecrets(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1BulkCreateSecretsWithBody request with any body - V1BulkCreateSecretsWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1BulkCreateSecrets(ctx context.Context, ref string, body V1BulkCreateSecretsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetSslEnforcementConfig request - V1GetSslEnforcementConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpdateSslEnforcementConfigWithBody request with any body - V1UpdateSslEnforcementConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpdateSslEnforcementConfig(ctx context.Context, ref string, body V1UpdateSslEnforcementConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ListAllBuckets request - V1ListAllBuckets(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GenerateTypescriptTypes request - V1GenerateTypescriptTypes(ctx context.Context, ref string, params *V1GenerateTypescriptTypesParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1UpgradePostgresVersionWithBody request with any body - V1UpgradePostgresVersionWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1UpgradePostgresVersion(ctx context.Context, ref string, body V1UpgradePostgresVersionJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetPostgresUpgradeEligibility request - V1GetPostgresUpgradeEligibility(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetPostgresUpgradeStatus request - V1GetPostgresUpgradeStatus(ctx context.Context, ref string, params *V1GetPostgresUpgradeStatusParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1DeactivateVanitySubdomainConfig request - V1DeactivateVanitySubdomainConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetVanitySubdomainConfig request - V1GetVanitySubdomainConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ActivateVanitySubdomainConfigWithBody request with any body - V1ActivateVanitySubdomainConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1ActivateVanitySubdomainConfig(ctx context.Context, ref string, body V1ActivateVanitySubdomainConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1CheckVanitySubdomainAvailabilityWithBody request with any body - V1CheckVanitySubdomainAvailabilityWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - V1CheckVanitySubdomainAvailability(ctx context.Context, ref string, body V1CheckVanitySubdomainAvailabilityJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1ListAllSnippets request - V1ListAllSnippets(ctx context.Context, params *V1ListAllSnippetsParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // V1GetASnippet request - V1GetASnippet(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) -} - -func (c *Client) V1DeleteABranch(ctx context.Context, branchIdOrRef string, params *V1DeleteABranchParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1DeleteABranchRequest(c.Server, branchIdOrRef, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetABranchConfig(ctx context.Context, branchIdOrRef string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetABranchConfigRequest(c.Server, branchIdOrRef) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateABranchConfigWithBody(ctx context.Context, branchIdOrRef string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateABranchConfigRequestWithBody(c.Server, branchIdOrRef, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateABranchConfig(ctx context.Context, branchIdOrRef string, body V1UpdateABranchConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateABranchConfigRequest(c.Server, branchIdOrRef, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1DiffABranch(ctx context.Context, branchIdOrRef string, params *V1DiffABranchParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1DiffABranchRequest(c.Server, branchIdOrRef, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1MergeABranchWithBody(ctx context.Context, branchIdOrRef string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1MergeABranchRequestWithBody(c.Server, branchIdOrRef, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1MergeABranch(ctx context.Context, branchIdOrRef string, body V1MergeABranchJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1MergeABranchRequest(c.Server, branchIdOrRef, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1PushABranchWithBody(ctx context.Context, branchIdOrRef string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1PushABranchRequestWithBody(c.Server, branchIdOrRef, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1PushABranch(ctx context.Context, branchIdOrRef string, body V1PushABranchJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1PushABranchRequest(c.Server, branchIdOrRef, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ResetABranchWithBody(ctx context.Context, branchIdOrRef string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ResetABranchRequestWithBody(c.Server, branchIdOrRef, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ResetABranch(ctx context.Context, branchIdOrRef string, body V1ResetABranchJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ResetABranchRequest(c.Server, branchIdOrRef, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1RestoreABranch(ctx context.Context, branchIdOrRef string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1RestoreABranchRequest(c.Server, branchIdOrRef) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1AuthorizeUser(ctx context.Context, params *V1AuthorizeUserParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1AuthorizeUserRequest(c.Server, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1OauthAuthorizeProjectClaim(ctx context.Context, params *V1OauthAuthorizeProjectClaimParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1OauthAuthorizeProjectClaimRequest(c.Server, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1RevokeTokenWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1RevokeTokenRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1RevokeToken(ctx context.Context, body V1RevokeTokenJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1RevokeTokenRequest(c.Server, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ExchangeOauthTokenWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ExchangeOauthTokenRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ExchangeOauthTokenWithFormdataBody(ctx context.Context, body V1ExchangeOauthTokenFormdataRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ExchangeOauthTokenRequestWithFormdataBody(c.Server, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ListAllOrganizations(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ListAllOrganizationsRequest(c.Server) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateAnOrganizationWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateAnOrganizationRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateAnOrganization(ctx context.Context, body V1CreateAnOrganizationJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateAnOrganizationRequest(c.Server, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetAnOrganization(ctx context.Context, slug string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetAnOrganizationRequest(c.Server, slug) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetOrganizationEntitlements(ctx context.Context, slug string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetOrganizationEntitlementsRequest(c.Server, slug) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ListOrganizationMembers(ctx context.Context, slug string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ListOrganizationMembersRequest(c.Server, slug) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetOrganizationProjectClaim(ctx context.Context, slug string, token string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetOrganizationProjectClaimRequest(c.Server, slug, token) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ClaimProjectForOrganization(ctx context.Context, slug string, token string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ClaimProjectForOrganizationRequest(c.Server, slug, token) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetAllProjectsForOrganization(ctx context.Context, slug string, params *V1GetAllProjectsForOrganizationParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetAllProjectsForOrganizationRequest(c.Server, slug, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetProfile(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetProfileRequest(c.Server) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ListAllProjects(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ListAllProjectsRequest(c.Server) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateAProjectWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateAProjectRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateAProject(ctx context.Context, body V1CreateAProjectJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateAProjectRequest(c.Server, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetAvailableRegions(ctx context.Context, params *V1GetAvailableRegionsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetAvailableRegionsRequest(c.Server, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1DeleteAProject(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1DeleteAProjectRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetProject(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetProjectRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateAProjectWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateAProjectRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateAProject(ctx context.Context, ref string, body V1UpdateAProjectJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateAProjectRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ListActionRuns(ctx context.Context, ref string, params *V1ListActionRunsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ListActionRunsRequest(c.Server, ref, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CountActionRuns(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CountActionRunsRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetActionRun(ctx context.Context, ref string, runId string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetActionRunRequest(c.Server, ref, runId) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetActionRunLogs(ctx context.Context, ref string, runId string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetActionRunLogsRequest(c.Server, ref, runId) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateActionRunStatusWithBody(ctx context.Context, ref string, runId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateActionRunStatusRequestWithBody(c.Server, ref, runId, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateActionRunStatus(ctx context.Context, ref string, runId string, body V1UpdateActionRunStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateActionRunStatusRequest(c.Server, ref, runId, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetPerformanceAdvisors(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetPerformanceAdvisorsRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetSecurityAdvisors(ctx context.Context, ref string, params *V1GetSecurityAdvisorsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetSecurityAdvisorsRequest(c.Server, ref, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetProjectFunctionCombinedStats(ctx context.Context, ref string, params *V1GetProjectFunctionCombinedStatsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetProjectFunctionCombinedStatsRequest(c.Server, ref, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetProjectLogs(ctx context.Context, ref string, params *V1GetProjectLogsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetProjectLogsRequest(c.Server, ref, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetProjectLogsAll(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetProjectLogsAllRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ScrapeProjectMetrics(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ScrapeProjectMetricsRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetProjectUsageApiCount(ctx context.Context, ref string, params *V1GetProjectUsageApiCountParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetProjectUsageApiCountRequest(c.Server, ref, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetProjectUsageRequestCount(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetProjectUsageRequestCountRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetProjectApiKeys(ctx context.Context, ref string, params *V1GetProjectApiKeysParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetProjectApiKeysRequest(c.Server, ref, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateProjectApiKeyWithBody(ctx context.Context, ref string, params *V1CreateProjectApiKeyParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateProjectApiKeyRequestWithBody(c.Server, ref, params, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateProjectApiKey(ctx context.Context, ref string, params *V1CreateProjectApiKeyParams, body V1CreateProjectApiKeyJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateProjectApiKeyRequest(c.Server, ref, params, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetProjectLegacyApiKeys(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetProjectLegacyApiKeysRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateProjectLegacyApiKeys(ctx context.Context, ref string, params *V1UpdateProjectLegacyApiKeysParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateProjectLegacyApiKeysRequest(c.Server, ref, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1DeleteProjectApiKey(ctx context.Context, ref string, id openapi_types.UUID, params *V1DeleteProjectApiKeyParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1DeleteProjectApiKeyRequest(c.Server, ref, id, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetProjectApiKey(ctx context.Context, ref string, id openapi_types.UUID, params *V1GetProjectApiKeyParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetProjectApiKeyRequest(c.Server, ref, id, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateProjectApiKeyWithBody(ctx context.Context, ref string, id openapi_types.UUID, params *V1UpdateProjectApiKeyParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateProjectApiKeyRequestWithBody(c.Server, ref, id, params, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateProjectApiKey(ctx context.Context, ref string, id openapi_types.UUID, params *V1UpdateProjectApiKeyParams, body V1UpdateProjectApiKeyJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateProjectApiKeyRequest(c.Server, ref, id, params, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ListProjectAddons(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ListProjectAddonsRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ApplyProjectAddonWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ApplyProjectAddonRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ApplyProjectAddon(ctx context.Context, ref string, body V1ApplyProjectAddonJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ApplyProjectAddonRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1RemoveProjectAddon(ctx context.Context, ref string, addonVariant struct { - union json.RawMessage -}, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1RemoveProjectAddonRequest(c.Server, ref, addonVariant) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1DisablePreviewBranching(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1DisablePreviewBranchingRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ListAllBranches(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ListAllBranchesRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateABranchWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateABranchRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateABranch(ctx context.Context, ref string, body V1CreateABranchJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateABranchRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetABranch(ctx context.Context, ref string, name string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetABranchRequest(c.Server, ref, name) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1DeleteProjectClaimToken(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1DeleteProjectClaimTokenRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetProjectClaimToken(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetProjectClaimTokenRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateProjectClaimToken(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateProjectClaimTokenRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1DeleteLoginRoles(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1DeleteLoginRolesRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateLoginRoleWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateLoginRoleRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateLoginRole(ctx context.Context, ref string, body V1CreateLoginRoleJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateLoginRoleRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetAuthServiceConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetAuthServiceConfigRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateAuthServiceConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateAuthServiceConfigRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateAuthServiceConfig(ctx context.Context, ref string, body V1UpdateAuthServiceConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateAuthServiceConfigRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetProjectSigningKeys(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetProjectSigningKeysRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateProjectSigningKeyWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateProjectSigningKeyRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateProjectSigningKey(ctx context.Context, ref string, body V1CreateProjectSigningKeyJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateProjectSigningKeyRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetLegacySigningKey(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetLegacySigningKeyRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateLegacySigningKey(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateLegacySigningKeyRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1RemoveProjectSigningKey(ctx context.Context, ref string, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1RemoveProjectSigningKeyRequest(c.Server, ref, id) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetProjectSigningKey(ctx context.Context, ref string, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetProjectSigningKeyRequest(c.Server, ref, id) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateProjectSigningKeyWithBody(ctx context.Context, ref string, id openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateProjectSigningKeyRequestWithBody(c.Server, ref, id, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateProjectSigningKey(ctx context.Context, ref string, id openapi_types.UUID, body V1UpdateProjectSigningKeyJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateProjectSigningKeyRequest(c.Server, ref, id, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ListAllSsoProvider(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ListAllSsoProviderRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateASsoProviderWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateASsoProviderRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateASsoProvider(ctx context.Context, ref string, body V1CreateASsoProviderJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateASsoProviderRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1DeleteASsoProvider(ctx context.Context, ref string, providerId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1DeleteASsoProviderRequest(c.Server, ref, providerId) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetASsoProvider(ctx context.Context, ref string, providerId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetASsoProviderRequest(c.Server, ref, providerId) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateASsoProviderWithBody(ctx context.Context, ref string, providerId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateASsoProviderRequestWithBody(c.Server, ref, providerId, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateASsoProvider(ctx context.Context, ref string, providerId openapi_types.UUID, body V1UpdateASsoProviderJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateASsoProviderRequest(c.Server, ref, providerId, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ListProjectTpaIntegrations(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ListProjectTpaIntegrationsRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateProjectTpaIntegrationWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateProjectTpaIntegrationRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateProjectTpaIntegration(ctx context.Context, ref string, body V1CreateProjectTpaIntegrationJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateProjectTpaIntegrationRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1DeleteProjectTpaIntegration(ctx context.Context, ref string, tpaId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1DeleteProjectTpaIntegrationRequest(c.Server, ref, tpaId) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetProjectTpaIntegration(ctx context.Context, ref string, tpaId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetProjectTpaIntegrationRequest(c.Server, ref, tpaId) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetProjectPgbouncerConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetProjectPgbouncerConfigRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetPoolerConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetPoolerConfigRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdatePoolerConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdatePoolerConfigRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdatePoolerConfig(ctx context.Context, ref string, body V1UpdatePoolerConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdatePoolerConfigRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetPostgresConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetPostgresConfigRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdatePostgresConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdatePostgresConfigRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdatePostgresConfig(ctx context.Context, ref string, body V1UpdatePostgresConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdatePostgresConfigRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetDatabaseDisk(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetDatabaseDiskRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ModifyDatabaseDiskWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ModifyDatabaseDiskRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ModifyDatabaseDisk(ctx context.Context, ref string, body V1ModifyDatabaseDiskJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ModifyDatabaseDiskRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetProjectDiskAutoscaleConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetProjectDiskAutoscaleConfigRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetDiskUtilization(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetDiskUtilizationRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetRealtimeConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetRealtimeConfigRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateRealtimeConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateRealtimeConfigRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateRealtimeConfig(ctx context.Context, ref string, body V1UpdateRealtimeConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateRealtimeConfigRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ShutdownRealtime(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ShutdownRealtimeRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetStorageConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetStorageConfigRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateStorageConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateStorageConfigRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateStorageConfig(ctx context.Context, ref string, body V1UpdateStorageConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateStorageConfigRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1DeleteHostnameConfig(ctx context.Context, ref string, params *V1DeleteHostnameConfigParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1DeleteHostnameConfigRequest(c.Server, ref, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetHostnameConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetHostnameConfigRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ActivateCustomHostname(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ActivateCustomHostnameRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateHostnameConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateHostnameConfigRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateHostnameConfig(ctx context.Context, ref string, body V1UpdateHostnameConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateHostnameConfigRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1VerifyDnsConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1VerifyDnsConfigRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ListAllBackups(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ListAllBackupsRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1RestorePhysicalBackupWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1RestorePhysicalBackupRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1RestorePhysicalBackup(ctx context.Context, ref string, body V1RestorePhysicalBackupJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1RestorePhysicalBackupRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1RestorePitrBackupWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1RestorePitrBackupRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1RestorePitrBackup(ctx context.Context, ref string, body V1RestorePitrBackupJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1RestorePitrBackupRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetRestorePoint(ctx context.Context, ref string, params *V1GetRestorePointParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetRestorePointRequest(c.Server, ref, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateRestorePointWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateRestorePointRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateRestorePoint(ctx context.Context, ref string, body V1CreateRestorePointJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateRestorePointRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetBackupSchedule(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetBackupScheduleRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateBackupScheduleWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateBackupScheduleRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateBackupSchedule(ctx context.Context, ref string, body V1UpdateBackupScheduleJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateBackupScheduleRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UndoWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UndoRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1Undo(ctx context.Context, ref string, body V1UndoJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UndoRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetDatabaseMetadata(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetDatabaseMetadataRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetJitAccess(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetJitAccessRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1AuthorizeJitAccessWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1AuthorizeJitAccessRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1AuthorizeJitAccess(ctx context.Context, ref string, body V1AuthorizeJitAccessJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1AuthorizeJitAccessRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateJitAccessWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateJitAccessRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateJitAccess(ctx context.Context, ref string, body V1UpdateJitAccessJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateJitAccessRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1InviteExternalJitAccessWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1InviteExternalJitAccessRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1InviteExternalJitAccess(ctx context.Context, ref string, body V1InviteExternalJitAccessJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1InviteExternalJitAccessRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1AcceptInviteExternalJitAccessWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1AcceptInviteExternalJitAccessRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1AcceptInviteExternalJitAccess(ctx context.Context, ref string, body V1AcceptInviteExternalJitAccessJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1AcceptInviteExternalJitAccessRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1DeleteInviteExternalJitAccess(ctx context.Context, ref string, inviteId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1DeleteInviteExternalJitAccessRequest(c.Server, ref, inviteId) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ListJitAccess(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ListJitAccessRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1DeleteJitAccess(ctx context.Context, ref string, userId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1DeleteJitAccessRequest(c.Server, ref, userId) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1RollbackMigrations(ctx context.Context, ref string, params *V1RollbackMigrationsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1RollbackMigrationsRequest(c.Server, ref, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ListMigrationHistory(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ListMigrationHistoryRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ApplyAMigrationWithBody(ctx context.Context, ref string, params *V1ApplyAMigrationParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ApplyAMigrationRequestWithBody(c.Server, ref, params, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ApplyAMigration(ctx context.Context, ref string, params *V1ApplyAMigrationParams, body V1ApplyAMigrationJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ApplyAMigrationRequest(c.Server, ref, params, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpsertAMigrationWithBody(ctx context.Context, ref string, params *V1UpsertAMigrationParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpsertAMigrationRequestWithBody(c.Server, ref, params, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpsertAMigration(ctx context.Context, ref string, params *V1UpsertAMigrationParams, body V1UpsertAMigrationJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpsertAMigrationRequest(c.Server, ref, params, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetAMigration(ctx context.Context, ref string, version string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetAMigrationRequest(c.Server, ref, version) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1PatchAMigrationWithBody(ctx context.Context, ref string, version string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1PatchAMigrationRequestWithBody(c.Server, ref, version, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1PatchAMigration(ctx context.Context, ref string, version string, body V1PatchAMigrationJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1PatchAMigrationRequest(c.Server, ref, version, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetDatabaseOpenapi(ctx context.Context, ref string, params *V1GetDatabaseOpenapiParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetDatabaseOpenapiRequest(c.Server, ref, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateDatabasePasswordWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateDatabasePasswordRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateDatabasePassword(ctx context.Context, ref string, body V1UpdateDatabasePasswordJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateDatabasePasswordRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1RunAQueryWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1RunAQueryRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1RunAQuery(ctx context.Context, ref string, body V1RunAQueryJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1RunAQueryRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ReadOnlyQueryWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ReadOnlyQueryRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ReadOnlyQuery(ctx context.Context, ref string, body V1ReadOnlyQueryJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ReadOnlyQueryRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1EnableDatabaseWebhook(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1EnableDatabaseWebhookRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ListAllFunctions(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ListAllFunctionsRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateAFunctionWithBody(ctx context.Context, ref string, params *V1CreateAFunctionParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateAFunctionRequestWithBody(c.Server, ref, params, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CreateAFunction(ctx context.Context, ref string, params *V1CreateAFunctionParams, body V1CreateAFunctionJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CreateAFunctionRequest(c.Server, ref, params, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1BulkUpdateFunctionsWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1BulkUpdateFunctionsRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1BulkUpdateFunctions(ctx context.Context, ref string, body V1BulkUpdateFunctionsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1BulkUpdateFunctionsRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1DeployAFunctionWithBody(ctx context.Context, ref string, params *V1DeployAFunctionParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1DeployAFunctionRequestWithBody(c.Server, ref, params, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1DeleteAFunction(ctx context.Context, ref string, functionSlug string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1DeleteAFunctionRequest(c.Server, ref, functionSlug) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetAFunction(ctx context.Context, ref string, functionSlug string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetAFunctionRequest(c.Server, ref, functionSlug) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateAFunctionWithBody(ctx context.Context, ref string, functionSlug string, params *V1UpdateAFunctionParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateAFunctionRequestWithBody(c.Server, ref, functionSlug, params, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateAFunction(ctx context.Context, ref string, functionSlug string, params *V1UpdateAFunctionParams, body V1UpdateAFunctionJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateAFunctionRequest(c.Server, ref, functionSlug, params, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetAFunctionBody(ctx context.Context, ref string, functionSlug string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetAFunctionBodyRequest(c.Server, ref, functionSlug) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetServicesHealth(ctx context.Context, ref string, params *V1GetServicesHealthParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetServicesHealthRequest(c.Server, ref, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetJitAccessConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetJitAccessConfigRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateJitAccessConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateJitAccessConfigRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateJitAccessConfig(ctx context.Context, ref string, body V1UpdateJitAccessConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateJitAccessConfigRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1DeleteNetworkBansWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1DeleteNetworkBansRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1DeleteNetworkBans(ctx context.Context, ref string, body V1DeleteNetworkBansJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1DeleteNetworkBansRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ListAllNetworkBans(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ListAllNetworkBansRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ListAllNetworkBansEnriched(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ListAllNetworkBansEnrichedRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetNetworkRestrictions(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetNetworkRestrictionsRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1PatchNetworkRestrictionsWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1PatchNetworkRestrictionsRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1PatchNetworkRestrictions(ctx context.Context, ref string, body V1PatchNetworkRestrictionsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1PatchNetworkRestrictionsRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateNetworkRestrictionsWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateNetworkRestrictionsRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateNetworkRestrictions(ctx context.Context, ref string, body V1UpdateNetworkRestrictionsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateNetworkRestrictionsRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1PauseAProject(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1PauseAProjectRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetPgsodiumConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetPgsodiumConfigRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdatePgsodiumConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdatePgsodiumConfigRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdatePgsodiumConfig(ctx context.Context, ref string, body V1UpdatePgsodiumConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdatePgsodiumConfigRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetPostgrestServiceConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetPostgrestServiceConfigRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdatePostgrestServiceConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdatePostgrestServiceConfigRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdatePostgrestServiceConfig(ctx context.Context, ref string, body V1UpdatePostgrestServiceConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdatePostgrestServiceConfigRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1RemoveAReadReplicaWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1RemoveAReadReplicaRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1RemoveAReadReplica(ctx context.Context, ref string, body V1RemoveAReadReplicaJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1RemoveAReadReplicaRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1SetupAReadReplicaWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1SetupAReadReplicaRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1SetupAReadReplica(ctx context.Context, ref string, body V1SetupAReadReplicaJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1SetupAReadReplicaRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetReadonlyModeStatus(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetReadonlyModeStatusRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1DisableReadonlyModeTemporarily(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1DisableReadonlyModeTemporarilyRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1RestartAProject(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1RestartAProjectRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ListAvailableRestoreVersions(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ListAvailableRestoreVersionsRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1RestoreAProject(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1RestoreAProjectRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CancelAProjectRestoration(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CancelAProjectRestorationRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1BulkDeleteSecretsWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1BulkDeleteSecretsRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1BulkDeleteSecrets(ctx context.Context, ref string, body V1BulkDeleteSecretsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1BulkDeleteSecretsRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ListAllSecrets(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ListAllSecretsRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1BulkCreateSecretsWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1BulkCreateSecretsRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1BulkCreateSecrets(ctx context.Context, ref string, body V1BulkCreateSecretsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1BulkCreateSecretsRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetSslEnforcementConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetSslEnforcementConfigRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateSslEnforcementConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateSslEnforcementConfigRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpdateSslEnforcementConfig(ctx context.Context, ref string, body V1UpdateSslEnforcementConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpdateSslEnforcementConfigRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ListAllBuckets(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ListAllBucketsRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GenerateTypescriptTypes(ctx context.Context, ref string, params *V1GenerateTypescriptTypesParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GenerateTypescriptTypesRequest(c.Server, ref, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpgradePostgresVersionWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpgradePostgresVersionRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1UpgradePostgresVersion(ctx context.Context, ref string, body V1UpgradePostgresVersionJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1UpgradePostgresVersionRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetPostgresUpgradeEligibility(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetPostgresUpgradeEligibilityRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetPostgresUpgradeStatus(ctx context.Context, ref string, params *V1GetPostgresUpgradeStatusParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetPostgresUpgradeStatusRequest(c.Server, ref, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1DeactivateVanitySubdomainConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1DeactivateVanitySubdomainConfigRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetVanitySubdomainConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetVanitySubdomainConfigRequest(c.Server, ref) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ActivateVanitySubdomainConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ActivateVanitySubdomainConfigRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ActivateVanitySubdomainConfig(ctx context.Context, ref string, body V1ActivateVanitySubdomainConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ActivateVanitySubdomainConfigRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CheckVanitySubdomainAvailabilityWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CheckVanitySubdomainAvailabilityRequestWithBody(c.Server, ref, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1CheckVanitySubdomainAvailability(ctx context.Context, ref string, body V1CheckVanitySubdomainAvailabilityJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1CheckVanitySubdomainAvailabilityRequest(c.Server, ref, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1ListAllSnippets(ctx context.Context, params *V1ListAllSnippetsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1ListAllSnippetsRequest(c.Server, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) V1GetASnippet(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewV1GetASnippetRequest(c.Server, id) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -// NewV1DeleteABranchRequest generates requests for V1DeleteABranch -func NewV1DeleteABranchRequest(server string, branchIdOrRef string, params *V1DeleteABranchParams) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "branch_id_or_ref", branchIdOrRef, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/branches/%s", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.Force != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "force", *params.Force, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetABranchConfigRequest generates requests for V1GetABranchConfig -func NewV1GetABranchConfigRequest(server string, branchIdOrRef string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "branch_id_or_ref", branchIdOrRef, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/branches/%s", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1UpdateABranchConfigRequest calls the generic V1UpdateABranchConfig builder with application/json body -func NewV1UpdateABranchConfigRequest(server string, branchIdOrRef string, body V1UpdateABranchConfigJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdateABranchConfigRequestWithBody(server, branchIdOrRef, "application/json", bodyReader) -} - -// NewV1UpdateABranchConfigRequestWithBody generates requests for V1UpdateABranchConfig with any type of body -func NewV1UpdateABranchConfigRequestWithBody(server string, branchIdOrRef string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "branch_id_or_ref", branchIdOrRef, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/branches/%s", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1DiffABranchRequest generates requests for V1DiffABranch -func NewV1DiffABranchRequest(server string, branchIdOrRef string, params *V1DiffABranchParams) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "branch_id_or_ref", branchIdOrRef, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/branches/%s/diff", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.IncludedSchemas != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "included_schemas", *params.IncludedSchemas, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.Pgdelta != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "pgdelta", *params.Pgdelta, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1MergeABranchRequest calls the generic V1MergeABranch builder with application/json body -func NewV1MergeABranchRequest(server string, branchIdOrRef string, body V1MergeABranchJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1MergeABranchRequestWithBody(server, branchIdOrRef, "application/json", bodyReader) -} - -// NewV1MergeABranchRequestWithBody generates requests for V1MergeABranch with any type of body -func NewV1MergeABranchRequestWithBody(server string, branchIdOrRef string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "branch_id_or_ref", branchIdOrRef, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/branches/%s/merge", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1PushABranchRequest calls the generic V1PushABranch builder with application/json body -func NewV1PushABranchRequest(server string, branchIdOrRef string, body V1PushABranchJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1PushABranchRequestWithBody(server, branchIdOrRef, "application/json", bodyReader) -} - -// NewV1PushABranchRequestWithBody generates requests for V1PushABranch with any type of body -func NewV1PushABranchRequestWithBody(server string, branchIdOrRef string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "branch_id_or_ref", branchIdOrRef, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/branches/%s/push", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1ResetABranchRequest calls the generic V1ResetABranch builder with application/json body -func NewV1ResetABranchRequest(server string, branchIdOrRef string, body V1ResetABranchJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1ResetABranchRequestWithBody(server, branchIdOrRef, "application/json", bodyReader) -} - -// NewV1ResetABranchRequestWithBody generates requests for V1ResetABranch with any type of body -func NewV1ResetABranchRequestWithBody(server string, branchIdOrRef string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "branch_id_or_ref", branchIdOrRef, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/branches/%s/reset", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1RestoreABranchRequest generates requests for V1RestoreABranch -func NewV1RestoreABranchRequest(server string, branchIdOrRef string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "branch_id_or_ref", branchIdOrRef, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/branches/%s/restore", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1AuthorizeUserRequest generates requests for V1AuthorizeUser -func NewV1AuthorizeUserRequest(server string, params *V1AuthorizeUserParams) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/oauth/authorize") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "client_id", params.ClientId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: "uuid"}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "response_type", params.ResponseType, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "redirect_uri", params.RedirectUri, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - if params.Scope != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "scope", *params.Scope, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.State != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "state", *params.State, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.ResponseMode != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "response_mode", *params.ResponseMode, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.CodeChallenge != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "code_challenge", *params.CodeChallenge, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.CodeChallengeMethod != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "code_challenge_method", *params.CodeChallengeMethod, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.OrganizationSlug != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "organization_slug", *params.OrganizationSlug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.TargetFlow != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "target_flow", *params.TargetFlow, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.Resource != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "resource", *params.Resource, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: "uri"}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1OauthAuthorizeProjectClaimRequest generates requests for V1OauthAuthorizeProjectClaim -func NewV1OauthAuthorizeProjectClaimRequest(server string, params *V1OauthAuthorizeProjectClaimParams) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/oauth/authorize/project-claim") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "project_ref", params.ProjectRef, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "client_id", params.ClientId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: "uuid"}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "response_type", params.ResponseType, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "redirect_uri", params.RedirectUri, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - if params.State != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "state", *params.State, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.ResponseMode != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "response_mode", *params.ResponseMode, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.CodeChallenge != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "code_challenge", *params.CodeChallenge, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.CodeChallengeMethod != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "code_challenge_method", *params.CodeChallengeMethod, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1RevokeTokenRequest calls the generic V1RevokeToken builder with application/json body -func NewV1RevokeTokenRequest(server string, body V1RevokeTokenJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1RevokeTokenRequestWithBody(server, "application/json", bodyReader) -} - -// NewV1RevokeTokenRequestWithBody generates requests for V1RevokeToken with any type of body -func NewV1RevokeTokenRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/oauth/revoke") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1ExchangeOauthTokenRequestWithFormdataBody calls the generic V1ExchangeOauthToken builder with application/x-www-form-urlencoded body -func NewV1ExchangeOauthTokenRequestWithFormdataBody(server string, body V1ExchangeOauthTokenFormdataRequestBody) (*http.Request, error) { - var bodyReader io.Reader - bodyStr, err := runtime.MarshalForm(body, nil) - if err != nil { - return nil, err - } - bodyReader = strings.NewReader(bodyStr.Encode()) - return NewV1ExchangeOauthTokenRequestWithBody(server, "application/x-www-form-urlencoded", bodyReader) -} - -// NewV1ExchangeOauthTokenRequestWithBody generates requests for V1ExchangeOauthToken with any type of body -func NewV1ExchangeOauthTokenRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/oauth/token") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1ListAllOrganizationsRequest generates requests for V1ListAllOrganizations -func NewV1ListAllOrganizationsRequest(server string) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/organizations") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1CreateAnOrganizationRequest calls the generic V1CreateAnOrganization builder with application/json body -func NewV1CreateAnOrganizationRequest(server string, body V1CreateAnOrganizationJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1CreateAnOrganizationRequestWithBody(server, "application/json", bodyReader) -} - -// NewV1CreateAnOrganizationRequestWithBody generates requests for V1CreateAnOrganization with any type of body -func NewV1CreateAnOrganizationRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/organizations") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1GetAnOrganizationRequest generates requests for V1GetAnOrganization -func NewV1GetAnOrganizationRequest(server string, slug string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "slug", slug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/organizations/%s", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetOrganizationEntitlementsRequest generates requests for V1GetOrganizationEntitlements -func NewV1GetOrganizationEntitlementsRequest(server string, slug string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "slug", slug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/organizations/%s/entitlements", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1ListOrganizationMembersRequest generates requests for V1ListOrganizationMembers -func NewV1ListOrganizationMembersRequest(server string, slug string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "slug", slug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/organizations/%s/members", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetOrganizationProjectClaimRequest generates requests for V1GetOrganizationProjectClaim -func NewV1GetOrganizationProjectClaimRequest(server string, slug string, token string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "slug", slug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "token", token, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/organizations/%s/project-claim/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1ClaimProjectForOrganizationRequest generates requests for V1ClaimProjectForOrganization -func NewV1ClaimProjectForOrganizationRequest(server string, slug string, token string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "slug", slug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "token", token, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/organizations/%s/project-claim/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetAllProjectsForOrganizationRequest generates requests for V1GetAllProjectsForOrganization -func NewV1GetAllProjectsForOrganizationRequest(server string, slug string, params *V1GetAllProjectsForOrganizationParams) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "slug", slug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/organizations/%s/projects", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.Offset != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "offset", *params.Offset, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.Limit != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "limit", *params.Limit, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.Search != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "search", *params.Search, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.Sort != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "sort", *params.Sort, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.Statuses != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "statuses", *params.Statuses, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetProfileRequest generates requests for V1GetProfile -func NewV1GetProfileRequest(server string) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/profile") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1ListAllProjectsRequest generates requests for V1ListAllProjects -func NewV1ListAllProjectsRequest(server string) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1CreateAProjectRequest calls the generic V1CreateAProject builder with application/json body -func NewV1CreateAProjectRequest(server string, body V1CreateAProjectJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1CreateAProjectRequestWithBody(server, "application/json", bodyReader) -} - -// NewV1CreateAProjectRequestWithBody generates requests for V1CreateAProject with any type of body -func NewV1CreateAProjectRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1GetAvailableRegionsRequest generates requests for V1GetAvailableRegions -func NewV1GetAvailableRegionsRequest(server string, params *V1GetAvailableRegionsParams) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/available-regions") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "organization_slug", params.OrganizationSlug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - if params.Continent != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "continent", *params.Continent, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.DesiredInstanceSize != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "desired_instance_size", *params.DesiredInstanceSize, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1DeleteAProjectRequest generates requests for V1DeleteAProject -func NewV1DeleteAProjectRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetProjectRequest generates requests for V1GetProject -func NewV1GetProjectRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1UpdateAProjectRequest calls the generic V1UpdateAProject builder with application/json body -func NewV1UpdateAProjectRequest(server string, ref string, body V1UpdateAProjectJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdateAProjectRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1UpdateAProjectRequestWithBody generates requests for V1UpdateAProject with any type of body -func NewV1UpdateAProjectRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1ListActionRunsRequest generates requests for V1ListActionRuns -func NewV1ListActionRunsRequest(server string, ref string, params *V1ListActionRunsParams) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/actions", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.Offset != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "offset", *params.Offset, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "number", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.Limit != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "limit", *params.Limit, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "number", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1CountActionRunsRequest generates requests for V1CountActionRuns -func NewV1CountActionRunsRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/actions", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodHead, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetActionRunRequest generates requests for V1GetActionRun -func NewV1GetActionRunRequest(server string, ref string, runId string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "run_id", runId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/actions/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetActionRunLogsRequest generates requests for V1GetActionRunLogs -func NewV1GetActionRunLogsRequest(server string, ref string, runId string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "run_id", runId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/actions/%s/logs", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1UpdateActionRunStatusRequest calls the generic V1UpdateActionRunStatus builder with application/json body -func NewV1UpdateActionRunStatusRequest(server string, ref string, runId string, body V1UpdateActionRunStatusJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdateActionRunStatusRequestWithBody(server, ref, runId, "application/json", bodyReader) -} - -// NewV1UpdateActionRunStatusRequestWithBody generates requests for V1UpdateActionRunStatus with any type of body -func NewV1UpdateActionRunStatusRequestWithBody(server string, ref string, runId string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "run_id", runId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/actions/%s/status", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1GetPerformanceAdvisorsRequest generates requests for V1GetPerformanceAdvisors -func NewV1GetPerformanceAdvisorsRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/advisors/performance", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetSecurityAdvisorsRequest generates requests for V1GetSecurityAdvisors -func NewV1GetSecurityAdvisorsRequest(server string, ref string, params *V1GetSecurityAdvisorsParams) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/advisors/security", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.LintType != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "lint_type", *params.LintType, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetProjectFunctionCombinedStatsRequest generates requests for V1GetProjectFunctionCombinedStats -func NewV1GetProjectFunctionCombinedStatsRequest(server string, ref string, params *V1GetProjectFunctionCombinedStatsParams) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/analytics/endpoints/functions.combined-stats", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "interval", params.Interval, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "function_id", params.FunctionId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetProjectLogsRequest generates requests for V1GetProjectLogs -func NewV1GetProjectLogsRequest(server string, ref string, params *V1GetProjectLogsParams) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/analytics/endpoints/logs", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.Sql != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "sql", *params.Sql, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.IsoTimestampStart != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "iso_timestamp_start", *params.IsoTimestampStart, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: "date-time"}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.IsoTimestampEnd != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "iso_timestamp_end", *params.IsoTimestampEnd, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: "date-time"}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetProjectLogsAllRequest generates requests for V1GetProjectLogsAll -func NewV1GetProjectLogsAllRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/analytics/endpoints/logs.all", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1ScrapeProjectMetricsRequest generates requests for V1ScrapeProjectMetrics -func NewV1ScrapeProjectMetricsRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/analytics/endpoints/metrics", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetProjectUsageApiCountRequest generates requests for V1GetProjectUsageApiCount -func NewV1GetProjectUsageApiCountRequest(server string, ref string, params *V1GetProjectUsageApiCountParams) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/analytics/endpoints/usage.api-counts", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.Interval != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "interval", *params.Interval, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetProjectUsageRequestCountRequest generates requests for V1GetProjectUsageRequestCount -func NewV1GetProjectUsageRequestCountRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/analytics/endpoints/usage.api-requests-count", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetProjectApiKeysRequest generates requests for V1GetProjectApiKeys -func NewV1GetProjectApiKeysRequest(server string, ref string, params *V1GetProjectApiKeysParams) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/api-keys", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.Reveal != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "reveal", *params.Reveal, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1CreateProjectApiKeyRequest calls the generic V1CreateProjectApiKey builder with application/json body -func NewV1CreateProjectApiKeyRequest(server string, ref string, params *V1CreateProjectApiKeyParams, body V1CreateProjectApiKeyJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1CreateProjectApiKeyRequestWithBody(server, ref, params, "application/json", bodyReader) -} - -// NewV1CreateProjectApiKeyRequestWithBody generates requests for V1CreateProjectApiKey with any type of body -func NewV1CreateProjectApiKeyRequestWithBody(server string, ref string, params *V1CreateProjectApiKeyParams, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/api-keys", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.Reveal != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "reveal", *params.Reveal, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1GetProjectLegacyApiKeysRequest generates requests for V1GetProjectLegacyApiKeys -func NewV1GetProjectLegacyApiKeysRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/api-keys/legacy", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1UpdateProjectLegacyApiKeysRequest generates requests for V1UpdateProjectLegacyApiKeys -func NewV1UpdateProjectLegacyApiKeysRequest(server string, ref string, params *V1UpdateProjectLegacyApiKeysParams) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/api-keys/legacy", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "enabled", params.Enabled, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodPut, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1DeleteProjectApiKeyRequest generates requests for V1DeleteProjectApiKey -func NewV1DeleteProjectApiKeyRequest(server string, ref string, id openapi_types.UUID, params *V1DeleteProjectApiKeyParams) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/api-keys/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.Reveal != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "reveal", *params.Reveal, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.WasCompromised != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "was_compromised", *params.WasCompromised, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.Reason != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "reason", *params.Reason, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetProjectApiKeyRequest generates requests for V1GetProjectApiKey -func NewV1GetProjectApiKeyRequest(server string, ref string, id openapi_types.UUID, params *V1GetProjectApiKeyParams) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/api-keys/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.Reveal != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "reveal", *params.Reveal, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1UpdateProjectApiKeyRequest calls the generic V1UpdateProjectApiKey builder with application/json body -func NewV1UpdateProjectApiKeyRequest(server string, ref string, id openapi_types.UUID, params *V1UpdateProjectApiKeyParams, body V1UpdateProjectApiKeyJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdateProjectApiKeyRequestWithBody(server, ref, id, params, "application/json", bodyReader) -} - -// NewV1UpdateProjectApiKeyRequestWithBody generates requests for V1UpdateProjectApiKey with any type of body -func NewV1UpdateProjectApiKeyRequestWithBody(server string, ref string, id openapi_types.UUID, params *V1UpdateProjectApiKeyParams, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/api-keys/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.Reveal != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "reveal", *params.Reveal, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1ListProjectAddonsRequest generates requests for V1ListProjectAddons -func NewV1ListProjectAddonsRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/billing/addons", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1ApplyProjectAddonRequest calls the generic V1ApplyProjectAddon builder with application/json body -func NewV1ApplyProjectAddonRequest(server string, ref string, body V1ApplyProjectAddonJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1ApplyProjectAddonRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1ApplyProjectAddonRequestWithBody generates requests for V1ApplyProjectAddon with any type of body -func NewV1ApplyProjectAddonRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/billing/addons", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1RemoveProjectAddonRequest generates requests for V1RemoveProjectAddon -func NewV1RemoveProjectAddonRequest(server string, ref string, addonVariant struct { - union json.RawMessage -}) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "addon_variant", addonVariant, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/billing/addons/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1DisablePreviewBranchingRequest generates requests for V1DisablePreviewBranching -func NewV1DisablePreviewBranchingRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/branches", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1ListAllBranchesRequest generates requests for V1ListAllBranches -func NewV1ListAllBranchesRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/branches", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1CreateABranchRequest calls the generic V1CreateABranch builder with application/json body -func NewV1CreateABranchRequest(server string, ref string, body V1CreateABranchJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1CreateABranchRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1CreateABranchRequestWithBody generates requests for V1CreateABranch with any type of body -func NewV1CreateABranchRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/branches", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1GetABranchRequest generates requests for V1GetABranch -func NewV1GetABranchRequest(server string, ref string, name string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "name", name, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/branches/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1DeleteProjectClaimTokenRequest generates requests for V1DeleteProjectClaimToken -func NewV1DeleteProjectClaimTokenRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/claim-token", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetProjectClaimTokenRequest generates requests for V1GetProjectClaimToken -func NewV1GetProjectClaimTokenRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/claim-token", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1CreateProjectClaimTokenRequest generates requests for V1CreateProjectClaimToken -func NewV1CreateProjectClaimTokenRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/claim-token", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1DeleteLoginRolesRequest generates requests for V1DeleteLoginRoles -func NewV1DeleteLoginRolesRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/cli/login-role", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1CreateLoginRoleRequest calls the generic V1CreateLoginRole builder with application/json body -func NewV1CreateLoginRoleRequest(server string, ref string, body V1CreateLoginRoleJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1CreateLoginRoleRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1CreateLoginRoleRequestWithBody generates requests for V1CreateLoginRole with any type of body -func NewV1CreateLoginRoleRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/cli/login-role", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1GetAuthServiceConfigRequest generates requests for V1GetAuthServiceConfig -func NewV1GetAuthServiceConfigRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/auth", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1UpdateAuthServiceConfigRequest calls the generic V1UpdateAuthServiceConfig builder with application/json body -func NewV1UpdateAuthServiceConfigRequest(server string, ref string, body V1UpdateAuthServiceConfigJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdateAuthServiceConfigRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1UpdateAuthServiceConfigRequestWithBody generates requests for V1UpdateAuthServiceConfig with any type of body -func NewV1UpdateAuthServiceConfigRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/auth", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1GetProjectSigningKeysRequest generates requests for V1GetProjectSigningKeys -func NewV1GetProjectSigningKeysRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/auth/signing-keys", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1CreateProjectSigningKeyRequest calls the generic V1CreateProjectSigningKey builder with application/json body -func NewV1CreateProjectSigningKeyRequest(server string, ref string, body V1CreateProjectSigningKeyJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1CreateProjectSigningKeyRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1CreateProjectSigningKeyRequestWithBody generates requests for V1CreateProjectSigningKey with any type of body -func NewV1CreateProjectSigningKeyRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/auth/signing-keys", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1GetLegacySigningKeyRequest generates requests for V1GetLegacySigningKey -func NewV1GetLegacySigningKeyRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/auth/signing-keys/legacy", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1CreateLegacySigningKeyRequest generates requests for V1CreateLegacySigningKey -func NewV1CreateLegacySigningKeyRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/auth/signing-keys/legacy", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1RemoveProjectSigningKeyRequest generates requests for V1RemoveProjectSigningKey -func NewV1RemoveProjectSigningKeyRequest(server string, ref string, id openapi_types.UUID) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/auth/signing-keys/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetProjectSigningKeyRequest generates requests for V1GetProjectSigningKey -func NewV1GetProjectSigningKeyRequest(server string, ref string, id openapi_types.UUID) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/auth/signing-keys/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1UpdateProjectSigningKeyRequest calls the generic V1UpdateProjectSigningKey builder with application/json body -func NewV1UpdateProjectSigningKeyRequest(server string, ref string, id openapi_types.UUID, body V1UpdateProjectSigningKeyJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdateProjectSigningKeyRequestWithBody(server, ref, id, "application/json", bodyReader) -} - -// NewV1UpdateProjectSigningKeyRequestWithBody generates requests for V1UpdateProjectSigningKey with any type of body -func NewV1UpdateProjectSigningKeyRequestWithBody(server string, ref string, id openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/auth/signing-keys/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1ListAllSsoProviderRequest generates requests for V1ListAllSsoProvider -func NewV1ListAllSsoProviderRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/auth/sso/providers", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1CreateASsoProviderRequest calls the generic V1CreateASsoProvider builder with application/json body -func NewV1CreateASsoProviderRequest(server string, ref string, body V1CreateASsoProviderJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1CreateASsoProviderRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1CreateASsoProviderRequestWithBody generates requests for V1CreateASsoProvider with any type of body -func NewV1CreateASsoProviderRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/auth/sso/providers", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1DeleteASsoProviderRequest generates requests for V1DeleteASsoProvider -func NewV1DeleteASsoProviderRequest(server string, ref string, providerId openapi_types.UUID) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "provider_id", providerId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/auth/sso/providers/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetASsoProviderRequest generates requests for V1GetASsoProvider -func NewV1GetASsoProviderRequest(server string, ref string, providerId openapi_types.UUID) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "provider_id", providerId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/auth/sso/providers/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1UpdateASsoProviderRequest calls the generic V1UpdateASsoProvider builder with application/json body -func NewV1UpdateASsoProviderRequest(server string, ref string, providerId openapi_types.UUID, body V1UpdateASsoProviderJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdateASsoProviderRequestWithBody(server, ref, providerId, "application/json", bodyReader) -} - -// NewV1UpdateASsoProviderRequestWithBody generates requests for V1UpdateASsoProvider with any type of body -func NewV1UpdateASsoProviderRequestWithBody(server string, ref string, providerId openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "provider_id", providerId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/auth/sso/providers/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPut, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1ListProjectTpaIntegrationsRequest generates requests for V1ListProjectTpaIntegrations -func NewV1ListProjectTpaIntegrationsRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/auth/third-party-auth", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1CreateProjectTpaIntegrationRequest calls the generic V1CreateProjectTpaIntegration builder with application/json body -func NewV1CreateProjectTpaIntegrationRequest(server string, ref string, body V1CreateProjectTpaIntegrationJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1CreateProjectTpaIntegrationRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1CreateProjectTpaIntegrationRequestWithBody generates requests for V1CreateProjectTpaIntegration with any type of body -func NewV1CreateProjectTpaIntegrationRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/auth/third-party-auth", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1DeleteProjectTpaIntegrationRequest generates requests for V1DeleteProjectTpaIntegration -func NewV1DeleteProjectTpaIntegrationRequest(server string, ref string, tpaId openapi_types.UUID) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "tpa_id", tpaId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/auth/third-party-auth/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetProjectTpaIntegrationRequest generates requests for V1GetProjectTpaIntegration -func NewV1GetProjectTpaIntegrationRequest(server string, ref string, tpaId openapi_types.UUID) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "tpa_id", tpaId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/auth/third-party-auth/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetProjectPgbouncerConfigRequest generates requests for V1GetProjectPgbouncerConfig -func NewV1GetProjectPgbouncerConfigRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/database/pgbouncer", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetPoolerConfigRequest generates requests for V1GetPoolerConfig -func NewV1GetPoolerConfigRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/database/pooler", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1UpdatePoolerConfigRequest calls the generic V1UpdatePoolerConfig builder with application/json body -func NewV1UpdatePoolerConfigRequest(server string, ref string, body V1UpdatePoolerConfigJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdatePoolerConfigRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1UpdatePoolerConfigRequestWithBody generates requests for V1UpdatePoolerConfig with any type of body -func NewV1UpdatePoolerConfigRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/database/pooler", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1GetPostgresConfigRequest generates requests for V1GetPostgresConfig -func NewV1GetPostgresConfigRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/database/postgres", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1UpdatePostgresConfigRequest calls the generic V1UpdatePostgresConfig builder with application/json body -func NewV1UpdatePostgresConfigRequest(server string, ref string, body V1UpdatePostgresConfigJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdatePostgresConfigRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1UpdatePostgresConfigRequestWithBody generates requests for V1UpdatePostgresConfig with any type of body -func NewV1UpdatePostgresConfigRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/database/postgres", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPut, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1GetDatabaseDiskRequest generates requests for V1GetDatabaseDisk -func NewV1GetDatabaseDiskRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/disk", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1ModifyDatabaseDiskRequest calls the generic V1ModifyDatabaseDisk builder with application/json body -func NewV1ModifyDatabaseDiskRequest(server string, ref string, body V1ModifyDatabaseDiskJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1ModifyDatabaseDiskRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1ModifyDatabaseDiskRequestWithBody generates requests for V1ModifyDatabaseDisk with any type of body -func NewV1ModifyDatabaseDiskRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/disk", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1GetProjectDiskAutoscaleConfigRequest generates requests for V1GetProjectDiskAutoscaleConfig -func NewV1GetProjectDiskAutoscaleConfigRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/disk/autoscale", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetDiskUtilizationRequest generates requests for V1GetDiskUtilization -func NewV1GetDiskUtilizationRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/disk/util", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetRealtimeConfigRequest generates requests for V1GetRealtimeConfig -func NewV1GetRealtimeConfigRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/realtime", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1UpdateRealtimeConfigRequest calls the generic V1UpdateRealtimeConfig builder with application/json body -func NewV1UpdateRealtimeConfigRequest(server string, ref string, body V1UpdateRealtimeConfigJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdateRealtimeConfigRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1UpdateRealtimeConfigRequestWithBody generates requests for V1UpdateRealtimeConfig with any type of body -func NewV1UpdateRealtimeConfigRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/realtime", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1ShutdownRealtimeRequest generates requests for V1ShutdownRealtime -func NewV1ShutdownRealtimeRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/realtime/shutdown", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetStorageConfigRequest generates requests for V1GetStorageConfig -func NewV1GetStorageConfigRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/storage", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1UpdateStorageConfigRequest calls the generic V1UpdateStorageConfig builder with application/json body -func NewV1UpdateStorageConfigRequest(server string, ref string, body V1UpdateStorageConfigJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdateStorageConfigRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1UpdateStorageConfigRequestWithBody generates requests for V1UpdateStorageConfig with any type of body -func NewV1UpdateStorageConfigRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/config/storage", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1DeleteHostnameConfigRequest generates requests for V1DeleteHostnameConfig -func NewV1DeleteHostnameConfigRequest(server string, ref string, params *V1DeleteHostnameConfigParams) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/custom-hostname", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.RemoveAddon != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "remove_addon", *params.RemoveAddon, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetHostnameConfigRequest generates requests for V1GetHostnameConfig -func NewV1GetHostnameConfigRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/custom-hostname", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1ActivateCustomHostnameRequest generates requests for V1ActivateCustomHostname -func NewV1ActivateCustomHostnameRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/custom-hostname/activate", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1UpdateHostnameConfigRequest calls the generic V1UpdateHostnameConfig builder with application/json body -func NewV1UpdateHostnameConfigRequest(server string, ref string, body V1UpdateHostnameConfigJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdateHostnameConfigRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1UpdateHostnameConfigRequestWithBody generates requests for V1UpdateHostnameConfig with any type of body -func NewV1UpdateHostnameConfigRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/custom-hostname/initialize", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1VerifyDnsConfigRequest generates requests for V1VerifyDnsConfig -func NewV1VerifyDnsConfigRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/custom-hostname/reverify", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1ListAllBackupsRequest generates requests for V1ListAllBackups -func NewV1ListAllBackupsRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/backups", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1RestorePhysicalBackupRequest calls the generic V1RestorePhysicalBackup builder with application/json body -func NewV1RestorePhysicalBackupRequest(server string, ref string, body V1RestorePhysicalBackupJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1RestorePhysicalBackupRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1RestorePhysicalBackupRequestWithBody generates requests for V1RestorePhysicalBackup with any type of body -func NewV1RestorePhysicalBackupRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/backups/restore", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1RestorePitrBackupRequest calls the generic V1RestorePitrBackup builder with application/json body -func NewV1RestorePitrBackupRequest(server string, ref string, body V1RestorePitrBackupJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1RestorePitrBackupRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1RestorePitrBackupRequestWithBody generates requests for V1RestorePitrBackup with any type of body -func NewV1RestorePitrBackupRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/backups/restore-pitr", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1GetRestorePointRequest generates requests for V1GetRestorePoint -func NewV1GetRestorePointRequest(server string, ref string, params *V1GetRestorePointParams) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/backups/restore-point", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.Name != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "name", *params.Name, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1CreateRestorePointRequest calls the generic V1CreateRestorePoint builder with application/json body -func NewV1CreateRestorePointRequest(server string, ref string, body V1CreateRestorePointJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1CreateRestorePointRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1CreateRestorePointRequestWithBody generates requests for V1CreateRestorePoint with any type of body -func NewV1CreateRestorePointRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/backups/restore-point", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1GetBackupScheduleRequest generates requests for V1GetBackupSchedule -func NewV1GetBackupScheduleRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/backups/schedule", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1UpdateBackupScheduleRequest calls the generic V1UpdateBackupSchedule builder with application/json body -func NewV1UpdateBackupScheduleRequest(server string, ref string, body V1UpdateBackupScheduleJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdateBackupScheduleRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1UpdateBackupScheduleRequestWithBody generates requests for V1UpdateBackupSchedule with any type of body -func NewV1UpdateBackupScheduleRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/backups/schedule", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1UndoRequest calls the generic V1Undo builder with application/json body -func NewV1UndoRequest(server string, ref string, body V1UndoJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UndoRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1UndoRequestWithBody generates requests for V1Undo with any type of body -func NewV1UndoRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/backups/undo", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1GetDatabaseMetadataRequest generates requests for V1GetDatabaseMetadata -func NewV1GetDatabaseMetadataRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/context", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetJitAccessRequest generates requests for V1GetJitAccess -func NewV1GetJitAccessRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/jit", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1AuthorizeJitAccessRequest calls the generic V1AuthorizeJitAccess builder with application/json body -func NewV1AuthorizeJitAccessRequest(server string, ref string, body V1AuthorizeJitAccessJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1AuthorizeJitAccessRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1AuthorizeJitAccessRequestWithBody generates requests for V1AuthorizeJitAccess with any type of body -func NewV1AuthorizeJitAccessRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/jit", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1UpdateJitAccessRequest calls the generic V1UpdateJitAccess builder with application/json body -func NewV1UpdateJitAccessRequest(server string, ref string, body V1UpdateJitAccessJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdateJitAccessRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1UpdateJitAccessRequestWithBody generates requests for V1UpdateJitAccess with any type of body -func NewV1UpdateJitAccessRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/jit", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPut, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1InviteExternalJitAccessRequest calls the generic V1InviteExternalJitAccess builder with application/json body -func NewV1InviteExternalJitAccessRequest(server string, ref string, body V1InviteExternalJitAccessJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1InviteExternalJitAccessRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1InviteExternalJitAccessRequestWithBody generates requests for V1InviteExternalJitAccess with any type of body -func NewV1InviteExternalJitAccessRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/jit/invite", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1AcceptInviteExternalJitAccessRequest calls the generic V1AcceptInviteExternalJitAccess builder with application/json body -func NewV1AcceptInviteExternalJitAccessRequest(server string, ref string, body V1AcceptInviteExternalJitAccessJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1AcceptInviteExternalJitAccessRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1AcceptInviteExternalJitAccessRequestWithBody generates requests for V1AcceptInviteExternalJitAccess with any type of body -func NewV1AcceptInviteExternalJitAccessRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/jit/invite/accept", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1DeleteInviteExternalJitAccessRequest generates requests for V1DeleteInviteExternalJitAccess -func NewV1DeleteInviteExternalJitAccessRequest(server string, ref string, inviteId openapi_types.UUID) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "invite_id", inviteId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/jit/invite/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1ListJitAccessRequest generates requests for V1ListJitAccess -func NewV1ListJitAccessRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/jit/list", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1DeleteJitAccessRequest generates requests for V1DeleteJitAccess -func NewV1DeleteJitAccessRequest(server string, ref string, userId openapi_types.UUID) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "user_id", userId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/jit/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1RollbackMigrationsRequest generates requests for V1RollbackMigrations -func NewV1RollbackMigrationsRequest(server string, ref string, params *V1RollbackMigrationsParams) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/migrations", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "gte", params.Gte, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1ListMigrationHistoryRequest generates requests for V1ListMigrationHistory -func NewV1ListMigrationHistoryRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/migrations", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1ApplyAMigrationRequest calls the generic V1ApplyAMigration builder with application/json body -func NewV1ApplyAMigrationRequest(server string, ref string, params *V1ApplyAMigrationParams, body V1ApplyAMigrationJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1ApplyAMigrationRequestWithBody(server, ref, params, "application/json", bodyReader) -} - -// NewV1ApplyAMigrationRequestWithBody generates requests for V1ApplyAMigration with any type of body -func NewV1ApplyAMigrationRequestWithBody(server string, ref string, params *V1ApplyAMigrationParams, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/migrations", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - if params != nil { - - if params.IdempotencyKey != nil { - var headerParam0 string - - headerParam0, err = runtime.StyleParamWithOptions("simple", false, "Idempotency-Key", *params.IdempotencyKey, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationHeader, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - req.Header.Set("Idempotency-Key", headerParam0) - } - - } - - return req, nil -} - -// NewV1UpsertAMigrationRequest calls the generic V1UpsertAMigration builder with application/json body -func NewV1UpsertAMigrationRequest(server string, ref string, params *V1UpsertAMigrationParams, body V1UpsertAMigrationJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpsertAMigrationRequestWithBody(server, ref, params, "application/json", bodyReader) -} - -// NewV1UpsertAMigrationRequestWithBody generates requests for V1UpsertAMigration with any type of body -func NewV1UpsertAMigrationRequestWithBody(server string, ref string, params *V1UpsertAMigrationParams, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/migrations", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPut, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - if params != nil { - - if params.IdempotencyKey != nil { - var headerParam0 string - - headerParam0, err = runtime.StyleParamWithOptions("simple", false, "Idempotency-Key", *params.IdempotencyKey, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationHeader, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - req.Header.Set("Idempotency-Key", headerParam0) - } - - } - - return req, nil -} - -// NewV1GetAMigrationRequest generates requests for V1GetAMigration -func NewV1GetAMigrationRequest(server string, ref string, version string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "version", version, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/migrations/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1PatchAMigrationRequest calls the generic V1PatchAMigration builder with application/json body -func NewV1PatchAMigrationRequest(server string, ref string, version string, body V1PatchAMigrationJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1PatchAMigrationRequestWithBody(server, ref, version, "application/json", bodyReader) -} - -// NewV1PatchAMigrationRequestWithBody generates requests for V1PatchAMigration with any type of body -func NewV1PatchAMigrationRequestWithBody(server string, ref string, version string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "version", version, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/migrations/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1GetDatabaseOpenapiRequest generates requests for V1GetDatabaseOpenapi -func NewV1GetDatabaseOpenapiRequest(server string, ref string, params *V1GetDatabaseOpenapiParams) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/openapi", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.Schema != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "schema", *params.Schema, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1UpdateDatabasePasswordRequest calls the generic V1UpdateDatabasePassword builder with application/json body -func NewV1UpdateDatabasePasswordRequest(server string, ref string, body V1UpdateDatabasePasswordJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdateDatabasePasswordRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1UpdateDatabasePasswordRequestWithBody generates requests for V1UpdateDatabasePassword with any type of body -func NewV1UpdateDatabasePasswordRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/password", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1RunAQueryRequest calls the generic V1RunAQuery builder with application/json body -func NewV1RunAQueryRequest(server string, ref string, body V1RunAQueryJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1RunAQueryRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1RunAQueryRequestWithBody generates requests for V1RunAQuery with any type of body -func NewV1RunAQueryRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/query", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1ReadOnlyQueryRequest calls the generic V1ReadOnlyQuery builder with application/json body -func NewV1ReadOnlyQueryRequest(server string, ref string, body V1ReadOnlyQueryJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1ReadOnlyQueryRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1ReadOnlyQueryRequestWithBody generates requests for V1ReadOnlyQuery with any type of body -func NewV1ReadOnlyQueryRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/query/read-only", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1EnableDatabaseWebhookRequest generates requests for V1EnableDatabaseWebhook -func NewV1EnableDatabaseWebhookRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/database/webhooks/enable", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1ListAllFunctionsRequest generates requests for V1ListAllFunctions -func NewV1ListAllFunctionsRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/functions", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1CreateAFunctionRequest calls the generic V1CreateAFunction builder with application/json body -func NewV1CreateAFunctionRequest(server string, ref string, params *V1CreateAFunctionParams, body V1CreateAFunctionJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1CreateAFunctionRequestWithBody(server, ref, params, "application/json", bodyReader) -} - -// NewV1CreateAFunctionRequestWithBody generates requests for V1CreateAFunction with any type of body -func NewV1CreateAFunctionRequestWithBody(server string, ref string, params *V1CreateAFunctionParams, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/functions", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.Slug != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "slug", *params.Slug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.Name != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "name", *params.Name, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.VerifyJwt != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "verify_jwt", *params.VerifyJwt, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.ImportMap != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "import_map", *params.ImportMap, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.EntrypointPath != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "entrypoint_path", *params.EntrypointPath, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.ImportMapPath != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "import_map_path", *params.ImportMapPath, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.EzbrSha256 != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "ezbr_sha256", *params.EzbrSha256, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1BulkUpdateFunctionsRequest calls the generic V1BulkUpdateFunctions builder with application/json body -func NewV1BulkUpdateFunctionsRequest(server string, ref string, body V1BulkUpdateFunctionsJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1BulkUpdateFunctionsRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1BulkUpdateFunctionsRequestWithBody generates requests for V1BulkUpdateFunctions with any type of body -func NewV1BulkUpdateFunctionsRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/functions", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPut, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1DeployAFunctionRequestWithBody generates requests for V1DeployAFunction with any type of body -func NewV1DeployAFunctionRequestWithBody(server string, ref string, params *V1DeployAFunctionParams, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/functions/deploy", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.Slug != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "slug", *params.Slug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.BundleOnly != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "bundleOnly", *params.BundleOnly, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1DeleteAFunctionRequest generates requests for V1DeleteAFunction -func NewV1DeleteAFunctionRequest(server string, ref string, functionSlug string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "function_slug", functionSlug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/functions/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetAFunctionRequest generates requests for V1GetAFunction -func NewV1GetAFunctionRequest(server string, ref string, functionSlug string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "function_slug", functionSlug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/functions/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1UpdateAFunctionRequest calls the generic V1UpdateAFunction builder with application/json body -func NewV1UpdateAFunctionRequest(server string, ref string, functionSlug string, params *V1UpdateAFunctionParams, body V1UpdateAFunctionJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdateAFunctionRequestWithBody(server, ref, functionSlug, params, "application/json", bodyReader) -} - -// NewV1UpdateAFunctionRequestWithBody generates requests for V1UpdateAFunction with any type of body -func NewV1UpdateAFunctionRequestWithBody(server string, ref string, functionSlug string, params *V1UpdateAFunctionParams, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "function_slug", functionSlug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/functions/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.Slug != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "slug", *params.Slug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.Name != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "name", *params.Name, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.VerifyJwt != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "verify_jwt", *params.VerifyJwt, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.ImportMap != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "import_map", *params.ImportMap, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.EntrypointPath != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "entrypoint_path", *params.EntrypointPath, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.ImportMapPath != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "import_map_path", *params.ImportMapPath, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.EzbrSha256 != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "ezbr_sha256", *params.EzbrSha256, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1GetAFunctionBodyRequest generates requests for V1GetAFunctionBody -func NewV1GetAFunctionBodyRequest(server string, ref string, functionSlug string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "function_slug", functionSlug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/functions/%s/body", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetServicesHealthRequest generates requests for V1GetServicesHealth -func NewV1GetServicesHealthRequest(server string, ref string, params *V1GetServicesHealthParams) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/health", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.Services != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "services", params.Services, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "array", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.TimeoutMs != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "timeout_ms", *params.TimeoutMs, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetJitAccessConfigRequest generates requests for V1GetJitAccessConfig -func NewV1GetJitAccessConfigRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/jit-access", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1UpdateJitAccessConfigRequest calls the generic V1UpdateJitAccessConfig builder with application/json body -func NewV1UpdateJitAccessConfigRequest(server string, ref string, body V1UpdateJitAccessConfigJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdateJitAccessConfigRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1UpdateJitAccessConfigRequestWithBody generates requests for V1UpdateJitAccessConfig with any type of body -func NewV1UpdateJitAccessConfigRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/jit-access", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPut, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1DeleteNetworkBansRequest calls the generic V1DeleteNetworkBans builder with application/json body -func NewV1DeleteNetworkBansRequest(server string, ref string, body V1DeleteNetworkBansJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1DeleteNetworkBansRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1DeleteNetworkBansRequestWithBody generates requests for V1DeleteNetworkBans with any type of body -func NewV1DeleteNetworkBansRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/network-bans", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodDelete, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1ListAllNetworkBansRequest generates requests for V1ListAllNetworkBans -func NewV1ListAllNetworkBansRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/network-bans/retrieve", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1ListAllNetworkBansEnrichedRequest generates requests for V1ListAllNetworkBansEnriched -func NewV1ListAllNetworkBansEnrichedRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/network-bans/retrieve/enriched", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetNetworkRestrictionsRequest generates requests for V1GetNetworkRestrictions -func NewV1GetNetworkRestrictionsRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/network-restrictions", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1PatchNetworkRestrictionsRequest calls the generic V1PatchNetworkRestrictions builder with application/json body -func NewV1PatchNetworkRestrictionsRequest(server string, ref string, body V1PatchNetworkRestrictionsJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1PatchNetworkRestrictionsRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1PatchNetworkRestrictionsRequestWithBody generates requests for V1PatchNetworkRestrictions with any type of body -func NewV1PatchNetworkRestrictionsRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/network-restrictions", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1UpdateNetworkRestrictionsRequest calls the generic V1UpdateNetworkRestrictions builder with application/json body -func NewV1UpdateNetworkRestrictionsRequest(server string, ref string, body V1UpdateNetworkRestrictionsJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdateNetworkRestrictionsRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1UpdateNetworkRestrictionsRequestWithBody generates requests for V1UpdateNetworkRestrictions with any type of body -func NewV1UpdateNetworkRestrictionsRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/network-restrictions/apply", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1PauseAProjectRequest generates requests for V1PauseAProject -func NewV1PauseAProjectRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/pause", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetPgsodiumConfigRequest generates requests for V1GetPgsodiumConfig -func NewV1GetPgsodiumConfigRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/pgsodium", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1UpdatePgsodiumConfigRequest calls the generic V1UpdatePgsodiumConfig builder with application/json body -func NewV1UpdatePgsodiumConfigRequest(server string, ref string, body V1UpdatePgsodiumConfigJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdatePgsodiumConfigRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1UpdatePgsodiumConfigRequestWithBody generates requests for V1UpdatePgsodiumConfig with any type of body -func NewV1UpdatePgsodiumConfigRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/pgsodium", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPut, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1GetPostgrestServiceConfigRequest generates requests for V1GetPostgrestServiceConfig -func NewV1GetPostgrestServiceConfigRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/postgrest", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1UpdatePostgrestServiceConfigRequest calls the generic V1UpdatePostgrestServiceConfig builder with application/json body -func NewV1UpdatePostgrestServiceConfigRequest(server string, ref string, body V1UpdatePostgrestServiceConfigJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdatePostgrestServiceConfigRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1UpdatePostgrestServiceConfigRequestWithBody generates requests for V1UpdatePostgrestServiceConfig with any type of body -func NewV1UpdatePostgrestServiceConfigRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/postgrest", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1RemoveAReadReplicaRequest calls the generic V1RemoveAReadReplica builder with application/json body -func NewV1RemoveAReadReplicaRequest(server string, ref string, body V1RemoveAReadReplicaJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1RemoveAReadReplicaRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1RemoveAReadReplicaRequestWithBody generates requests for V1RemoveAReadReplica with any type of body -func NewV1RemoveAReadReplicaRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/read-replicas/remove", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1SetupAReadReplicaRequest calls the generic V1SetupAReadReplica builder with application/json body -func NewV1SetupAReadReplicaRequest(server string, ref string, body V1SetupAReadReplicaJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1SetupAReadReplicaRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1SetupAReadReplicaRequestWithBody generates requests for V1SetupAReadReplica with any type of body -func NewV1SetupAReadReplicaRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/read-replicas/setup", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1GetReadonlyModeStatusRequest generates requests for V1GetReadonlyModeStatus -func NewV1GetReadonlyModeStatusRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/readonly", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1DisableReadonlyModeTemporarilyRequest generates requests for V1DisableReadonlyModeTemporarily -func NewV1DisableReadonlyModeTemporarilyRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/readonly/temporary-disable", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1RestartAProjectRequest generates requests for V1RestartAProject -func NewV1RestartAProjectRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/restart", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1ListAvailableRestoreVersionsRequest generates requests for V1ListAvailableRestoreVersions -func NewV1ListAvailableRestoreVersionsRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/restore", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1RestoreAProjectRequest generates requests for V1RestoreAProject -func NewV1RestoreAProjectRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/restore", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1CancelAProjectRestorationRequest generates requests for V1CancelAProjectRestoration -func NewV1CancelAProjectRestorationRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/restore/cancel", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1BulkDeleteSecretsRequest calls the generic V1BulkDeleteSecrets builder with application/json body -func NewV1BulkDeleteSecretsRequest(server string, ref string, body V1BulkDeleteSecretsJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1BulkDeleteSecretsRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1BulkDeleteSecretsRequestWithBody generates requests for V1BulkDeleteSecrets with any type of body -func NewV1BulkDeleteSecretsRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/secrets", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodDelete, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1ListAllSecretsRequest generates requests for V1ListAllSecrets -func NewV1ListAllSecretsRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/secrets", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1BulkCreateSecretsRequest calls the generic V1BulkCreateSecrets builder with application/json body -func NewV1BulkCreateSecretsRequest(server string, ref string, body V1BulkCreateSecretsJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1BulkCreateSecretsRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1BulkCreateSecretsRequestWithBody generates requests for V1BulkCreateSecrets with any type of body -func NewV1BulkCreateSecretsRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/secrets", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1GetSslEnforcementConfigRequest generates requests for V1GetSslEnforcementConfig -func NewV1GetSslEnforcementConfigRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/ssl-enforcement", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1UpdateSslEnforcementConfigRequest calls the generic V1UpdateSslEnforcementConfig builder with application/json body -func NewV1UpdateSslEnforcementConfigRequest(server string, ref string, body V1UpdateSslEnforcementConfigJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdateSslEnforcementConfigRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1UpdateSslEnforcementConfigRequestWithBody generates requests for V1UpdateSslEnforcementConfig with any type of body -func NewV1UpdateSslEnforcementConfigRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/ssl-enforcement", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPut, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1ListAllBucketsRequest generates requests for V1ListAllBuckets -func NewV1ListAllBucketsRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/storage/buckets", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GenerateTypescriptTypesRequest generates requests for V1GenerateTypescriptTypes -func NewV1GenerateTypescriptTypesRequest(server string, ref string, params *V1GenerateTypescriptTypesParams) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/types/typescript", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.IncludedSchemas != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "included_schemas", *params.IncludedSchemas, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1UpgradePostgresVersionRequest calls the generic V1UpgradePostgresVersion builder with application/json body -func NewV1UpgradePostgresVersionRequest(server string, ref string, body V1UpgradePostgresVersionJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpgradePostgresVersionRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1UpgradePostgresVersionRequestWithBody generates requests for V1UpgradePostgresVersion with any type of body -func NewV1UpgradePostgresVersionRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/upgrade", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1GetPostgresUpgradeEligibilityRequest generates requests for V1GetPostgresUpgradeEligibility -func NewV1GetPostgresUpgradeEligibilityRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/upgrade/eligibility", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetPostgresUpgradeStatusRequest generates requests for V1GetPostgresUpgradeStatus -func NewV1GetPostgresUpgradeStatusRequest(server string, ref string, params *V1GetPostgresUpgradeStatusParams) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/upgrade/status", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.TrackingId != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "tracking_id", *params.TrackingId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1DeactivateVanitySubdomainConfigRequest generates requests for V1DeactivateVanitySubdomainConfig -func NewV1DeactivateVanitySubdomainConfigRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/vanity-subdomain", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetVanitySubdomainConfigRequest generates requests for V1GetVanitySubdomainConfig -func NewV1GetVanitySubdomainConfigRequest(server string, ref string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/vanity-subdomain", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1ActivateVanitySubdomainConfigRequest calls the generic V1ActivateVanitySubdomainConfig builder with application/json body -func NewV1ActivateVanitySubdomainConfigRequest(server string, ref string, body V1ActivateVanitySubdomainConfigJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1ActivateVanitySubdomainConfigRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1ActivateVanitySubdomainConfigRequestWithBody generates requests for V1ActivateVanitySubdomainConfig with any type of body -func NewV1ActivateVanitySubdomainConfigRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/vanity-subdomain/activate", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1CheckVanitySubdomainAvailabilityRequest calls the generic V1CheckVanitySubdomainAvailability builder with application/json body -func NewV1CheckVanitySubdomainAvailabilityRequest(server string, ref string, body V1CheckVanitySubdomainAvailabilityJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1CheckVanitySubdomainAvailabilityRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1CheckVanitySubdomainAvailabilityRequestWithBody generates requests for V1CheckVanitySubdomainAvailability with any type of body -func NewV1CheckVanitySubdomainAvailabilityRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/projects/%s/vanity-subdomain/check-availability", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewV1ListAllSnippetsRequest generates requests for V1ListAllSnippets -func NewV1ListAllSnippetsRequest(server string, params *V1ListAllSnippetsParams) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/snippets") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if params.ProjectRef != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "project_ref", *params.ProjectRef, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.Cursor != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "cursor", *params.Cursor, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.Limit != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "limit", *params.Limit, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.SortBy != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "sort_by", *params.SortBy, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.SortOrder != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "sort_order", *params.SortOrder, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewV1GetASnippetRequest generates requests for V1GetASnippet -func NewV1GetASnippetRequest(server string, id openapi_types.UUID) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/snippets/%s", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { - for _, r := range c.RequestEditors { - if err := r(ctx, req); err != nil { - return err - } - } - for _, r := range additionalEditors { - if err := r(ctx, req); err != nil { - return err - } - } - return nil -} - -// ClientWithResponses builds on ClientInterface to offer response payloads -type ClientWithResponses struct { - ClientInterface -} - -// NewClientWithResponses creates a new ClientWithResponses, which wraps -// Client with return type handling -func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { - client, err := NewClient(server, opts...) - if err != nil { - return nil, err - } - return &ClientWithResponses{client}, nil -} - -// WithBaseURL overrides the baseURL. -func WithBaseURL(baseURL string) ClientOption { - return func(c *Client) error { - newBaseURL, err := url.Parse(baseURL) - if err != nil { - return err - } - c.Server = newBaseURL.String() - return nil - } -} - -// ClientWithResponsesInterface is the interface specification for the client with responses above. -type ClientWithResponsesInterface interface { - // V1DeleteABranchWithResponse request - V1DeleteABranchWithResponse(ctx context.Context, branchIdOrRef string, params *V1DeleteABranchParams, reqEditors ...RequestEditorFn) (*V1DeleteABranchResponse, error) - - // V1GetABranchConfigWithResponse request - V1GetABranchConfigWithResponse(ctx context.Context, branchIdOrRef string, reqEditors ...RequestEditorFn) (*V1GetABranchConfigResponse, error) - - // V1UpdateABranchConfigWithBodyWithResponse request with any body - V1UpdateABranchConfigWithBodyWithResponse(ctx context.Context, branchIdOrRef string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateABranchConfigResponse, error) - - V1UpdateABranchConfigWithResponse(ctx context.Context, branchIdOrRef string, body V1UpdateABranchConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateABranchConfigResponse, error) - - // V1DiffABranchWithResponse request - V1DiffABranchWithResponse(ctx context.Context, branchIdOrRef string, params *V1DiffABranchParams, reqEditors ...RequestEditorFn) (*V1DiffABranchResponse, error) - - // V1MergeABranchWithBodyWithResponse request with any body - V1MergeABranchWithBodyWithResponse(ctx context.Context, branchIdOrRef string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1MergeABranchResponse, error) - - V1MergeABranchWithResponse(ctx context.Context, branchIdOrRef string, body V1MergeABranchJSONRequestBody, reqEditors ...RequestEditorFn) (*V1MergeABranchResponse, error) - - // V1PushABranchWithBodyWithResponse request with any body - V1PushABranchWithBodyWithResponse(ctx context.Context, branchIdOrRef string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1PushABranchResponse, error) - - V1PushABranchWithResponse(ctx context.Context, branchIdOrRef string, body V1PushABranchJSONRequestBody, reqEditors ...RequestEditorFn) (*V1PushABranchResponse, error) - - // V1ResetABranchWithBodyWithResponse request with any body - V1ResetABranchWithBodyWithResponse(ctx context.Context, branchIdOrRef string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1ResetABranchResponse, error) - - V1ResetABranchWithResponse(ctx context.Context, branchIdOrRef string, body V1ResetABranchJSONRequestBody, reqEditors ...RequestEditorFn) (*V1ResetABranchResponse, error) - - // V1RestoreABranchWithResponse request - V1RestoreABranchWithResponse(ctx context.Context, branchIdOrRef string, reqEditors ...RequestEditorFn) (*V1RestoreABranchResponse, error) - - // V1AuthorizeUserWithResponse request - V1AuthorizeUserWithResponse(ctx context.Context, params *V1AuthorizeUserParams, reqEditors ...RequestEditorFn) (*V1AuthorizeUserResponse, error) - - // V1OauthAuthorizeProjectClaimWithResponse request - V1OauthAuthorizeProjectClaimWithResponse(ctx context.Context, params *V1OauthAuthorizeProjectClaimParams, reqEditors ...RequestEditorFn) (*V1OauthAuthorizeProjectClaimResponse, error) - - // V1RevokeTokenWithBodyWithResponse request with any body - V1RevokeTokenWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1RevokeTokenResponse, error) - - V1RevokeTokenWithResponse(ctx context.Context, body V1RevokeTokenJSONRequestBody, reqEditors ...RequestEditorFn) (*V1RevokeTokenResponse, error) - - // V1ExchangeOauthTokenWithBodyWithResponse request with any body - V1ExchangeOauthTokenWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1ExchangeOauthTokenResponse, error) - - V1ExchangeOauthTokenWithFormdataBodyWithResponse(ctx context.Context, body V1ExchangeOauthTokenFormdataRequestBody, reqEditors ...RequestEditorFn) (*V1ExchangeOauthTokenResponse, error) - - // V1ListAllOrganizationsWithResponse request - V1ListAllOrganizationsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*V1ListAllOrganizationsResponse, error) - - // V1CreateAnOrganizationWithBodyWithResponse request with any body - V1CreateAnOrganizationWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CreateAnOrganizationResponse, error) - - V1CreateAnOrganizationWithResponse(ctx context.Context, body V1CreateAnOrganizationJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CreateAnOrganizationResponse, error) - - // V1GetAnOrganizationWithResponse request - V1GetAnOrganizationWithResponse(ctx context.Context, slug string, reqEditors ...RequestEditorFn) (*V1GetAnOrganizationResponse, error) - - // V1GetOrganizationEntitlementsWithResponse request - V1GetOrganizationEntitlementsWithResponse(ctx context.Context, slug string, reqEditors ...RequestEditorFn) (*V1GetOrganizationEntitlementsResponse, error) - - // V1ListOrganizationMembersWithResponse request - V1ListOrganizationMembersWithResponse(ctx context.Context, slug string, reqEditors ...RequestEditorFn) (*V1ListOrganizationMembersResponse, error) - - // V1GetOrganizationProjectClaimWithResponse request - V1GetOrganizationProjectClaimWithResponse(ctx context.Context, slug string, token string, reqEditors ...RequestEditorFn) (*V1GetOrganizationProjectClaimResponse, error) - - // V1ClaimProjectForOrganizationWithResponse request - V1ClaimProjectForOrganizationWithResponse(ctx context.Context, slug string, token string, reqEditors ...RequestEditorFn) (*V1ClaimProjectForOrganizationResponse, error) - - // V1GetAllProjectsForOrganizationWithResponse request - V1GetAllProjectsForOrganizationWithResponse(ctx context.Context, slug string, params *V1GetAllProjectsForOrganizationParams, reqEditors ...RequestEditorFn) (*V1GetAllProjectsForOrganizationResponse, error) - - // V1GetProfileWithResponse request - V1GetProfileWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*V1GetProfileResponse, error) - - // V1ListAllProjectsWithResponse request - V1ListAllProjectsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*V1ListAllProjectsResponse, error) - - // V1CreateAProjectWithBodyWithResponse request with any body - V1CreateAProjectWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CreateAProjectResponse, error) - - V1CreateAProjectWithResponse(ctx context.Context, body V1CreateAProjectJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CreateAProjectResponse, error) - - // V1GetAvailableRegionsWithResponse request - V1GetAvailableRegionsWithResponse(ctx context.Context, params *V1GetAvailableRegionsParams, reqEditors ...RequestEditorFn) (*V1GetAvailableRegionsResponse, error) - - // V1DeleteAProjectWithResponse request - V1DeleteAProjectWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1DeleteAProjectResponse, error) - - // V1GetProjectWithResponse request - V1GetProjectWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetProjectResponse, error) - - // V1UpdateAProjectWithBodyWithResponse request with any body - V1UpdateAProjectWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateAProjectResponse, error) - - V1UpdateAProjectWithResponse(ctx context.Context, ref string, body V1UpdateAProjectJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateAProjectResponse, error) - - // V1ListActionRunsWithResponse request - V1ListActionRunsWithResponse(ctx context.Context, ref string, params *V1ListActionRunsParams, reqEditors ...RequestEditorFn) (*V1ListActionRunsResponse, error) - - // V1CountActionRunsWithResponse request - V1CountActionRunsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1CountActionRunsResponse, error) - - // V1GetActionRunWithResponse request - V1GetActionRunWithResponse(ctx context.Context, ref string, runId string, reqEditors ...RequestEditorFn) (*V1GetActionRunResponse, error) - - // V1GetActionRunLogsWithResponse request - V1GetActionRunLogsWithResponse(ctx context.Context, ref string, runId string, reqEditors ...RequestEditorFn) (*V1GetActionRunLogsResponse, error) - - // V1UpdateActionRunStatusWithBodyWithResponse request with any body - V1UpdateActionRunStatusWithBodyWithResponse(ctx context.Context, ref string, runId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateActionRunStatusResponse, error) - - V1UpdateActionRunStatusWithResponse(ctx context.Context, ref string, runId string, body V1UpdateActionRunStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateActionRunStatusResponse, error) - - // V1GetPerformanceAdvisorsWithResponse request - V1GetPerformanceAdvisorsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetPerformanceAdvisorsResponse, error) - - // V1GetSecurityAdvisorsWithResponse request - V1GetSecurityAdvisorsWithResponse(ctx context.Context, ref string, params *V1GetSecurityAdvisorsParams, reqEditors ...RequestEditorFn) (*V1GetSecurityAdvisorsResponse, error) - - // V1GetProjectFunctionCombinedStatsWithResponse request - V1GetProjectFunctionCombinedStatsWithResponse(ctx context.Context, ref string, params *V1GetProjectFunctionCombinedStatsParams, reqEditors ...RequestEditorFn) (*V1GetProjectFunctionCombinedStatsResponse, error) - - // V1GetProjectLogsWithResponse request - V1GetProjectLogsWithResponse(ctx context.Context, ref string, params *V1GetProjectLogsParams, reqEditors ...RequestEditorFn) (*V1GetProjectLogsResponse, error) - - // V1GetProjectLogsAllWithResponse request - V1GetProjectLogsAllWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetProjectLogsAllResponse, error) - - // V1ScrapeProjectMetricsWithResponse request - V1ScrapeProjectMetricsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ScrapeProjectMetricsResponse, error) - - // V1GetProjectUsageApiCountWithResponse request - V1GetProjectUsageApiCountWithResponse(ctx context.Context, ref string, params *V1GetProjectUsageApiCountParams, reqEditors ...RequestEditorFn) (*V1GetProjectUsageApiCountResponse, error) - - // V1GetProjectUsageRequestCountWithResponse request - V1GetProjectUsageRequestCountWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetProjectUsageRequestCountResponse, error) - - // V1GetProjectApiKeysWithResponse request - V1GetProjectApiKeysWithResponse(ctx context.Context, ref string, params *V1GetProjectApiKeysParams, reqEditors ...RequestEditorFn) (*V1GetProjectApiKeysResponse, error) - - // V1CreateProjectApiKeyWithBodyWithResponse request with any body - V1CreateProjectApiKeyWithBodyWithResponse(ctx context.Context, ref string, params *V1CreateProjectApiKeyParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CreateProjectApiKeyResponse, error) - - V1CreateProjectApiKeyWithResponse(ctx context.Context, ref string, params *V1CreateProjectApiKeyParams, body V1CreateProjectApiKeyJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CreateProjectApiKeyResponse, error) - - // V1GetProjectLegacyApiKeysWithResponse request - V1GetProjectLegacyApiKeysWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetProjectLegacyApiKeysResponse, error) - - // V1UpdateProjectLegacyApiKeysWithResponse request - V1UpdateProjectLegacyApiKeysWithResponse(ctx context.Context, ref string, params *V1UpdateProjectLegacyApiKeysParams, reqEditors ...RequestEditorFn) (*V1UpdateProjectLegacyApiKeysResponse, error) - - // V1DeleteProjectApiKeyWithResponse request - V1DeleteProjectApiKeyWithResponse(ctx context.Context, ref string, id openapi_types.UUID, params *V1DeleteProjectApiKeyParams, reqEditors ...RequestEditorFn) (*V1DeleteProjectApiKeyResponse, error) - - // V1GetProjectApiKeyWithResponse request - V1GetProjectApiKeyWithResponse(ctx context.Context, ref string, id openapi_types.UUID, params *V1GetProjectApiKeyParams, reqEditors ...RequestEditorFn) (*V1GetProjectApiKeyResponse, error) - - // V1UpdateProjectApiKeyWithBodyWithResponse request with any body - V1UpdateProjectApiKeyWithBodyWithResponse(ctx context.Context, ref string, id openapi_types.UUID, params *V1UpdateProjectApiKeyParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateProjectApiKeyResponse, error) - - V1UpdateProjectApiKeyWithResponse(ctx context.Context, ref string, id openapi_types.UUID, params *V1UpdateProjectApiKeyParams, body V1UpdateProjectApiKeyJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateProjectApiKeyResponse, error) - - // V1ListProjectAddonsWithResponse request - V1ListProjectAddonsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListProjectAddonsResponse, error) - - // V1ApplyProjectAddonWithBodyWithResponse request with any body - V1ApplyProjectAddonWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1ApplyProjectAddonResponse, error) - - V1ApplyProjectAddonWithResponse(ctx context.Context, ref string, body V1ApplyProjectAddonJSONRequestBody, reqEditors ...RequestEditorFn) (*V1ApplyProjectAddonResponse, error) - - // V1RemoveProjectAddonWithResponse request - V1RemoveProjectAddonWithResponse(ctx context.Context, ref string, addonVariant struct { - union json.RawMessage - }, reqEditors ...RequestEditorFn) (*V1RemoveProjectAddonResponse, error) - - // V1DisablePreviewBranchingWithResponse request - V1DisablePreviewBranchingWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1DisablePreviewBranchingResponse, error) - - // V1ListAllBranchesWithResponse request - V1ListAllBranchesWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListAllBranchesResponse, error) - - // V1CreateABranchWithBodyWithResponse request with any body - V1CreateABranchWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CreateABranchResponse, error) - - V1CreateABranchWithResponse(ctx context.Context, ref string, body V1CreateABranchJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CreateABranchResponse, error) - - // V1GetABranchWithResponse request - V1GetABranchWithResponse(ctx context.Context, ref string, name string, reqEditors ...RequestEditorFn) (*V1GetABranchResponse, error) - - // V1DeleteProjectClaimTokenWithResponse request - V1DeleteProjectClaimTokenWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1DeleteProjectClaimTokenResponse, error) - - // V1GetProjectClaimTokenWithResponse request - V1GetProjectClaimTokenWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetProjectClaimTokenResponse, error) - - // V1CreateProjectClaimTokenWithResponse request - V1CreateProjectClaimTokenWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1CreateProjectClaimTokenResponse, error) - - // V1DeleteLoginRolesWithResponse request - V1DeleteLoginRolesWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1DeleteLoginRolesResponse, error) - - // V1CreateLoginRoleWithBodyWithResponse request with any body - V1CreateLoginRoleWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CreateLoginRoleResponse, error) - - V1CreateLoginRoleWithResponse(ctx context.Context, ref string, body V1CreateLoginRoleJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CreateLoginRoleResponse, error) - - // V1GetAuthServiceConfigWithResponse request - V1GetAuthServiceConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetAuthServiceConfigResponse, error) - - // V1UpdateAuthServiceConfigWithBodyWithResponse request with any body - V1UpdateAuthServiceConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateAuthServiceConfigResponse, error) - - V1UpdateAuthServiceConfigWithResponse(ctx context.Context, ref string, body V1UpdateAuthServiceConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateAuthServiceConfigResponse, error) - - // V1GetProjectSigningKeysWithResponse request - V1GetProjectSigningKeysWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetProjectSigningKeysResponse, error) - - // V1CreateProjectSigningKeyWithBodyWithResponse request with any body - V1CreateProjectSigningKeyWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CreateProjectSigningKeyResponse, error) - - V1CreateProjectSigningKeyWithResponse(ctx context.Context, ref string, body V1CreateProjectSigningKeyJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CreateProjectSigningKeyResponse, error) - - // V1GetLegacySigningKeyWithResponse request - V1GetLegacySigningKeyWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetLegacySigningKeyResponse, error) - - // V1CreateLegacySigningKeyWithResponse request - V1CreateLegacySigningKeyWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1CreateLegacySigningKeyResponse, error) - - // V1RemoveProjectSigningKeyWithResponse request - V1RemoveProjectSigningKeyWithResponse(ctx context.Context, ref string, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*V1RemoveProjectSigningKeyResponse, error) - - // V1GetProjectSigningKeyWithResponse request - V1GetProjectSigningKeyWithResponse(ctx context.Context, ref string, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*V1GetProjectSigningKeyResponse, error) - - // V1UpdateProjectSigningKeyWithBodyWithResponse request with any body - V1UpdateProjectSigningKeyWithBodyWithResponse(ctx context.Context, ref string, id openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateProjectSigningKeyResponse, error) - - V1UpdateProjectSigningKeyWithResponse(ctx context.Context, ref string, id openapi_types.UUID, body V1UpdateProjectSigningKeyJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateProjectSigningKeyResponse, error) - - // V1ListAllSsoProviderWithResponse request - V1ListAllSsoProviderWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListAllSsoProviderResponse, error) - - // V1CreateASsoProviderWithBodyWithResponse request with any body - V1CreateASsoProviderWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CreateASsoProviderResponse, error) - - V1CreateASsoProviderWithResponse(ctx context.Context, ref string, body V1CreateASsoProviderJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CreateASsoProviderResponse, error) - - // V1DeleteASsoProviderWithResponse request - V1DeleteASsoProviderWithResponse(ctx context.Context, ref string, providerId openapi_types.UUID, reqEditors ...RequestEditorFn) (*V1DeleteASsoProviderResponse, error) - - // V1GetASsoProviderWithResponse request - V1GetASsoProviderWithResponse(ctx context.Context, ref string, providerId openapi_types.UUID, reqEditors ...RequestEditorFn) (*V1GetASsoProviderResponse, error) - - // V1UpdateASsoProviderWithBodyWithResponse request with any body - V1UpdateASsoProviderWithBodyWithResponse(ctx context.Context, ref string, providerId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateASsoProviderResponse, error) - - V1UpdateASsoProviderWithResponse(ctx context.Context, ref string, providerId openapi_types.UUID, body V1UpdateASsoProviderJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateASsoProviderResponse, error) - - // V1ListProjectTpaIntegrationsWithResponse request - V1ListProjectTpaIntegrationsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListProjectTpaIntegrationsResponse, error) - - // V1CreateProjectTpaIntegrationWithBodyWithResponse request with any body - V1CreateProjectTpaIntegrationWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CreateProjectTpaIntegrationResponse, error) - - V1CreateProjectTpaIntegrationWithResponse(ctx context.Context, ref string, body V1CreateProjectTpaIntegrationJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CreateProjectTpaIntegrationResponse, error) - - // V1DeleteProjectTpaIntegrationWithResponse request - V1DeleteProjectTpaIntegrationWithResponse(ctx context.Context, ref string, tpaId openapi_types.UUID, reqEditors ...RequestEditorFn) (*V1DeleteProjectTpaIntegrationResponse, error) - - // V1GetProjectTpaIntegrationWithResponse request - V1GetProjectTpaIntegrationWithResponse(ctx context.Context, ref string, tpaId openapi_types.UUID, reqEditors ...RequestEditorFn) (*V1GetProjectTpaIntegrationResponse, error) - - // V1GetProjectPgbouncerConfigWithResponse request - V1GetProjectPgbouncerConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetProjectPgbouncerConfigResponse, error) - - // V1GetPoolerConfigWithResponse request - V1GetPoolerConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetPoolerConfigResponse, error) - - // V1UpdatePoolerConfigWithBodyWithResponse request with any body - V1UpdatePoolerConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdatePoolerConfigResponse, error) - - V1UpdatePoolerConfigWithResponse(ctx context.Context, ref string, body V1UpdatePoolerConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdatePoolerConfigResponse, error) - - // V1GetPostgresConfigWithResponse request - V1GetPostgresConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetPostgresConfigResponse, error) - - // V1UpdatePostgresConfigWithBodyWithResponse request with any body - V1UpdatePostgresConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdatePostgresConfigResponse, error) - - V1UpdatePostgresConfigWithResponse(ctx context.Context, ref string, body V1UpdatePostgresConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdatePostgresConfigResponse, error) - - // V1GetDatabaseDiskWithResponse request - V1GetDatabaseDiskWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetDatabaseDiskResponse, error) - - // V1ModifyDatabaseDiskWithBodyWithResponse request with any body - V1ModifyDatabaseDiskWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1ModifyDatabaseDiskResponse, error) - - V1ModifyDatabaseDiskWithResponse(ctx context.Context, ref string, body V1ModifyDatabaseDiskJSONRequestBody, reqEditors ...RequestEditorFn) (*V1ModifyDatabaseDiskResponse, error) - - // V1GetProjectDiskAutoscaleConfigWithResponse request - V1GetProjectDiskAutoscaleConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetProjectDiskAutoscaleConfigResponse, error) - - // V1GetDiskUtilizationWithResponse request - V1GetDiskUtilizationWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetDiskUtilizationResponse, error) - - // V1GetRealtimeConfigWithResponse request - V1GetRealtimeConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetRealtimeConfigResponse, error) - - // V1UpdateRealtimeConfigWithBodyWithResponse request with any body - V1UpdateRealtimeConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateRealtimeConfigResponse, error) - - V1UpdateRealtimeConfigWithResponse(ctx context.Context, ref string, body V1UpdateRealtimeConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateRealtimeConfigResponse, error) - - // V1ShutdownRealtimeWithResponse request - V1ShutdownRealtimeWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ShutdownRealtimeResponse, error) - - // V1GetStorageConfigWithResponse request - V1GetStorageConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetStorageConfigResponse, error) - - // V1UpdateStorageConfigWithBodyWithResponse request with any body - V1UpdateStorageConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateStorageConfigResponse, error) - - V1UpdateStorageConfigWithResponse(ctx context.Context, ref string, body V1UpdateStorageConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateStorageConfigResponse, error) - - // V1DeleteHostnameConfigWithResponse request - V1DeleteHostnameConfigWithResponse(ctx context.Context, ref string, params *V1DeleteHostnameConfigParams, reqEditors ...RequestEditorFn) (*V1DeleteHostnameConfigResponse, error) - - // V1GetHostnameConfigWithResponse request - V1GetHostnameConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetHostnameConfigResponse, error) - - // V1ActivateCustomHostnameWithResponse request - V1ActivateCustomHostnameWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ActivateCustomHostnameResponse, error) - - // V1UpdateHostnameConfigWithBodyWithResponse request with any body - V1UpdateHostnameConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateHostnameConfigResponse, error) - - V1UpdateHostnameConfigWithResponse(ctx context.Context, ref string, body V1UpdateHostnameConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateHostnameConfigResponse, error) - - // V1VerifyDnsConfigWithResponse request - V1VerifyDnsConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1VerifyDnsConfigResponse, error) - - // V1ListAllBackupsWithResponse request - V1ListAllBackupsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListAllBackupsResponse, error) - - // V1RestorePhysicalBackupWithBodyWithResponse request with any body - V1RestorePhysicalBackupWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1RestorePhysicalBackupResponse, error) - - V1RestorePhysicalBackupWithResponse(ctx context.Context, ref string, body V1RestorePhysicalBackupJSONRequestBody, reqEditors ...RequestEditorFn) (*V1RestorePhysicalBackupResponse, error) - - // V1RestorePitrBackupWithBodyWithResponse request with any body - V1RestorePitrBackupWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1RestorePitrBackupResponse, error) - - V1RestorePitrBackupWithResponse(ctx context.Context, ref string, body V1RestorePitrBackupJSONRequestBody, reqEditors ...RequestEditorFn) (*V1RestorePitrBackupResponse, error) - - // V1GetRestorePointWithResponse request - V1GetRestorePointWithResponse(ctx context.Context, ref string, params *V1GetRestorePointParams, reqEditors ...RequestEditorFn) (*V1GetRestorePointResponse, error) - - // V1CreateRestorePointWithBodyWithResponse request with any body - V1CreateRestorePointWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CreateRestorePointResponse, error) - - V1CreateRestorePointWithResponse(ctx context.Context, ref string, body V1CreateRestorePointJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CreateRestorePointResponse, error) - - // V1GetBackupScheduleWithResponse request - V1GetBackupScheduleWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetBackupScheduleResponse, error) - - // V1UpdateBackupScheduleWithBodyWithResponse request with any body - V1UpdateBackupScheduleWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateBackupScheduleResponse, error) - - V1UpdateBackupScheduleWithResponse(ctx context.Context, ref string, body V1UpdateBackupScheduleJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateBackupScheduleResponse, error) - - // V1UndoWithBodyWithResponse request with any body - V1UndoWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UndoResponse, error) - - V1UndoWithResponse(ctx context.Context, ref string, body V1UndoJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UndoResponse, error) - - // V1GetDatabaseMetadataWithResponse request - V1GetDatabaseMetadataWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetDatabaseMetadataResponse, error) - - // V1GetJitAccessWithResponse request - V1GetJitAccessWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetJitAccessResponse, error) - - // V1AuthorizeJitAccessWithBodyWithResponse request with any body - V1AuthorizeJitAccessWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1AuthorizeJitAccessResponse, error) - - V1AuthorizeJitAccessWithResponse(ctx context.Context, ref string, body V1AuthorizeJitAccessJSONRequestBody, reqEditors ...RequestEditorFn) (*V1AuthorizeJitAccessResponse, error) - - // V1UpdateJitAccessWithBodyWithResponse request with any body - V1UpdateJitAccessWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateJitAccessResponse, error) - - V1UpdateJitAccessWithResponse(ctx context.Context, ref string, body V1UpdateJitAccessJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateJitAccessResponse, error) - - // V1InviteExternalJitAccessWithBodyWithResponse request with any body - V1InviteExternalJitAccessWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1InviteExternalJitAccessResponse, error) - - V1InviteExternalJitAccessWithResponse(ctx context.Context, ref string, body V1InviteExternalJitAccessJSONRequestBody, reqEditors ...RequestEditorFn) (*V1InviteExternalJitAccessResponse, error) - - // V1AcceptInviteExternalJitAccessWithBodyWithResponse request with any body - V1AcceptInviteExternalJitAccessWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1AcceptInviteExternalJitAccessResponse, error) - - V1AcceptInviteExternalJitAccessWithResponse(ctx context.Context, ref string, body V1AcceptInviteExternalJitAccessJSONRequestBody, reqEditors ...RequestEditorFn) (*V1AcceptInviteExternalJitAccessResponse, error) - - // V1DeleteInviteExternalJitAccessWithResponse request - V1DeleteInviteExternalJitAccessWithResponse(ctx context.Context, ref string, inviteId openapi_types.UUID, reqEditors ...RequestEditorFn) (*V1DeleteInviteExternalJitAccessResponse, error) - - // V1ListJitAccessWithResponse request - V1ListJitAccessWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListJitAccessResponse, error) - - // V1DeleteJitAccessWithResponse request - V1DeleteJitAccessWithResponse(ctx context.Context, ref string, userId openapi_types.UUID, reqEditors ...RequestEditorFn) (*V1DeleteJitAccessResponse, error) - - // V1RollbackMigrationsWithResponse request - V1RollbackMigrationsWithResponse(ctx context.Context, ref string, params *V1RollbackMigrationsParams, reqEditors ...RequestEditorFn) (*V1RollbackMigrationsResponse, error) - - // V1ListMigrationHistoryWithResponse request - V1ListMigrationHistoryWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListMigrationHistoryResponse, error) - - // V1ApplyAMigrationWithBodyWithResponse request with any body - V1ApplyAMigrationWithBodyWithResponse(ctx context.Context, ref string, params *V1ApplyAMigrationParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1ApplyAMigrationResponse, error) - - V1ApplyAMigrationWithResponse(ctx context.Context, ref string, params *V1ApplyAMigrationParams, body V1ApplyAMigrationJSONRequestBody, reqEditors ...RequestEditorFn) (*V1ApplyAMigrationResponse, error) - - // V1UpsertAMigrationWithBodyWithResponse request with any body - V1UpsertAMigrationWithBodyWithResponse(ctx context.Context, ref string, params *V1UpsertAMigrationParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpsertAMigrationResponse, error) - - V1UpsertAMigrationWithResponse(ctx context.Context, ref string, params *V1UpsertAMigrationParams, body V1UpsertAMigrationJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpsertAMigrationResponse, error) - - // V1GetAMigrationWithResponse request - V1GetAMigrationWithResponse(ctx context.Context, ref string, version string, reqEditors ...RequestEditorFn) (*V1GetAMigrationResponse, error) - - // V1PatchAMigrationWithBodyWithResponse request with any body - V1PatchAMigrationWithBodyWithResponse(ctx context.Context, ref string, version string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1PatchAMigrationResponse, error) - - V1PatchAMigrationWithResponse(ctx context.Context, ref string, version string, body V1PatchAMigrationJSONRequestBody, reqEditors ...RequestEditorFn) (*V1PatchAMigrationResponse, error) - - // V1GetDatabaseOpenapiWithResponse request - V1GetDatabaseOpenapiWithResponse(ctx context.Context, ref string, params *V1GetDatabaseOpenapiParams, reqEditors ...RequestEditorFn) (*V1GetDatabaseOpenapiResponse, error) - - // V1UpdateDatabasePasswordWithBodyWithResponse request with any body - V1UpdateDatabasePasswordWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateDatabasePasswordResponse, error) - - V1UpdateDatabasePasswordWithResponse(ctx context.Context, ref string, body V1UpdateDatabasePasswordJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateDatabasePasswordResponse, error) - - // V1RunAQueryWithBodyWithResponse request with any body - V1RunAQueryWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1RunAQueryResponse, error) - - V1RunAQueryWithResponse(ctx context.Context, ref string, body V1RunAQueryJSONRequestBody, reqEditors ...RequestEditorFn) (*V1RunAQueryResponse, error) - - // V1ReadOnlyQueryWithBodyWithResponse request with any body - V1ReadOnlyQueryWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1ReadOnlyQueryResponse, error) - - V1ReadOnlyQueryWithResponse(ctx context.Context, ref string, body V1ReadOnlyQueryJSONRequestBody, reqEditors ...RequestEditorFn) (*V1ReadOnlyQueryResponse, error) - - // V1EnableDatabaseWebhookWithResponse request - V1EnableDatabaseWebhookWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1EnableDatabaseWebhookResponse, error) - - // V1ListAllFunctionsWithResponse request - V1ListAllFunctionsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListAllFunctionsResponse, error) - - // V1CreateAFunctionWithBodyWithResponse request with any body - V1CreateAFunctionWithBodyWithResponse(ctx context.Context, ref string, params *V1CreateAFunctionParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CreateAFunctionResponse, error) - - V1CreateAFunctionWithResponse(ctx context.Context, ref string, params *V1CreateAFunctionParams, body V1CreateAFunctionJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CreateAFunctionResponse, error) - - // V1BulkUpdateFunctionsWithBodyWithResponse request with any body - V1BulkUpdateFunctionsWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1BulkUpdateFunctionsResponse, error) - - V1BulkUpdateFunctionsWithResponse(ctx context.Context, ref string, body V1BulkUpdateFunctionsJSONRequestBody, reqEditors ...RequestEditorFn) (*V1BulkUpdateFunctionsResponse, error) - - // V1DeployAFunctionWithBodyWithResponse request with any body - V1DeployAFunctionWithBodyWithResponse(ctx context.Context, ref string, params *V1DeployAFunctionParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1DeployAFunctionResponse, error) - - // V1DeleteAFunctionWithResponse request - V1DeleteAFunctionWithResponse(ctx context.Context, ref string, functionSlug string, reqEditors ...RequestEditorFn) (*V1DeleteAFunctionResponse, error) - - // V1GetAFunctionWithResponse request - V1GetAFunctionWithResponse(ctx context.Context, ref string, functionSlug string, reqEditors ...RequestEditorFn) (*V1GetAFunctionResponse, error) - - // V1UpdateAFunctionWithBodyWithResponse request with any body - V1UpdateAFunctionWithBodyWithResponse(ctx context.Context, ref string, functionSlug string, params *V1UpdateAFunctionParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateAFunctionResponse, error) - - V1UpdateAFunctionWithResponse(ctx context.Context, ref string, functionSlug string, params *V1UpdateAFunctionParams, body V1UpdateAFunctionJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateAFunctionResponse, error) - - // V1GetAFunctionBodyWithResponse request - V1GetAFunctionBodyWithResponse(ctx context.Context, ref string, functionSlug string, reqEditors ...RequestEditorFn) (*V1GetAFunctionBodyResponse, error) - - // V1GetServicesHealthWithResponse request - V1GetServicesHealthWithResponse(ctx context.Context, ref string, params *V1GetServicesHealthParams, reqEditors ...RequestEditorFn) (*V1GetServicesHealthResponse, error) - - // V1GetJitAccessConfigWithResponse request - V1GetJitAccessConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetJitAccessConfigResponse, error) - - // V1UpdateJitAccessConfigWithBodyWithResponse request with any body - V1UpdateJitAccessConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateJitAccessConfigResponse, error) - - V1UpdateJitAccessConfigWithResponse(ctx context.Context, ref string, body V1UpdateJitAccessConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateJitAccessConfigResponse, error) - - // V1DeleteNetworkBansWithBodyWithResponse request with any body - V1DeleteNetworkBansWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1DeleteNetworkBansResponse, error) - - V1DeleteNetworkBansWithResponse(ctx context.Context, ref string, body V1DeleteNetworkBansJSONRequestBody, reqEditors ...RequestEditorFn) (*V1DeleteNetworkBansResponse, error) - - // V1ListAllNetworkBansWithResponse request - V1ListAllNetworkBansWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListAllNetworkBansResponse, error) - - // V1ListAllNetworkBansEnrichedWithResponse request - V1ListAllNetworkBansEnrichedWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListAllNetworkBansEnrichedResponse, error) - - // V1GetNetworkRestrictionsWithResponse request - V1GetNetworkRestrictionsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetNetworkRestrictionsResponse, error) - - // V1PatchNetworkRestrictionsWithBodyWithResponse request with any body - V1PatchNetworkRestrictionsWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1PatchNetworkRestrictionsResponse, error) - - V1PatchNetworkRestrictionsWithResponse(ctx context.Context, ref string, body V1PatchNetworkRestrictionsJSONRequestBody, reqEditors ...RequestEditorFn) (*V1PatchNetworkRestrictionsResponse, error) - - // V1UpdateNetworkRestrictionsWithBodyWithResponse request with any body - V1UpdateNetworkRestrictionsWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateNetworkRestrictionsResponse, error) - - V1UpdateNetworkRestrictionsWithResponse(ctx context.Context, ref string, body V1UpdateNetworkRestrictionsJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateNetworkRestrictionsResponse, error) - - // V1PauseAProjectWithResponse request - V1PauseAProjectWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1PauseAProjectResponse, error) - - // V1GetPgsodiumConfigWithResponse request - V1GetPgsodiumConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetPgsodiumConfigResponse, error) - - // V1UpdatePgsodiumConfigWithBodyWithResponse request with any body - V1UpdatePgsodiumConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdatePgsodiumConfigResponse, error) - - V1UpdatePgsodiumConfigWithResponse(ctx context.Context, ref string, body V1UpdatePgsodiumConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdatePgsodiumConfigResponse, error) - - // V1GetPostgrestServiceConfigWithResponse request - V1GetPostgrestServiceConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetPostgrestServiceConfigResponse, error) - - // V1UpdatePostgrestServiceConfigWithBodyWithResponse request with any body - V1UpdatePostgrestServiceConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdatePostgrestServiceConfigResponse, error) - - V1UpdatePostgrestServiceConfigWithResponse(ctx context.Context, ref string, body V1UpdatePostgrestServiceConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdatePostgrestServiceConfigResponse, error) - - // V1RemoveAReadReplicaWithBodyWithResponse request with any body - V1RemoveAReadReplicaWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1RemoveAReadReplicaResponse, error) - - V1RemoveAReadReplicaWithResponse(ctx context.Context, ref string, body V1RemoveAReadReplicaJSONRequestBody, reqEditors ...RequestEditorFn) (*V1RemoveAReadReplicaResponse, error) - - // V1SetupAReadReplicaWithBodyWithResponse request with any body - V1SetupAReadReplicaWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1SetupAReadReplicaResponse, error) - - V1SetupAReadReplicaWithResponse(ctx context.Context, ref string, body V1SetupAReadReplicaJSONRequestBody, reqEditors ...RequestEditorFn) (*V1SetupAReadReplicaResponse, error) - - // V1GetReadonlyModeStatusWithResponse request - V1GetReadonlyModeStatusWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetReadonlyModeStatusResponse, error) - - // V1DisableReadonlyModeTemporarilyWithResponse request - V1DisableReadonlyModeTemporarilyWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1DisableReadonlyModeTemporarilyResponse, error) - - // V1RestartAProjectWithResponse request - V1RestartAProjectWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1RestartAProjectResponse, error) - - // V1ListAvailableRestoreVersionsWithResponse request - V1ListAvailableRestoreVersionsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListAvailableRestoreVersionsResponse, error) - - // V1RestoreAProjectWithResponse request - V1RestoreAProjectWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1RestoreAProjectResponse, error) - - // V1CancelAProjectRestorationWithResponse request - V1CancelAProjectRestorationWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1CancelAProjectRestorationResponse, error) - - // V1BulkDeleteSecretsWithBodyWithResponse request with any body - V1BulkDeleteSecretsWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1BulkDeleteSecretsResponse, error) - - V1BulkDeleteSecretsWithResponse(ctx context.Context, ref string, body V1BulkDeleteSecretsJSONRequestBody, reqEditors ...RequestEditorFn) (*V1BulkDeleteSecretsResponse, error) - - // V1ListAllSecretsWithResponse request - V1ListAllSecretsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListAllSecretsResponse, error) - - // V1BulkCreateSecretsWithBodyWithResponse request with any body - V1BulkCreateSecretsWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1BulkCreateSecretsResponse, error) - - V1BulkCreateSecretsWithResponse(ctx context.Context, ref string, body V1BulkCreateSecretsJSONRequestBody, reqEditors ...RequestEditorFn) (*V1BulkCreateSecretsResponse, error) - - // V1GetSslEnforcementConfigWithResponse request - V1GetSslEnforcementConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetSslEnforcementConfigResponse, error) - - // V1UpdateSslEnforcementConfigWithBodyWithResponse request with any body - V1UpdateSslEnforcementConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateSslEnforcementConfigResponse, error) - - V1UpdateSslEnforcementConfigWithResponse(ctx context.Context, ref string, body V1UpdateSslEnforcementConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateSslEnforcementConfigResponse, error) - - // V1ListAllBucketsWithResponse request - V1ListAllBucketsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListAllBucketsResponse, error) - - // V1GenerateTypescriptTypesWithResponse request - V1GenerateTypescriptTypesWithResponse(ctx context.Context, ref string, params *V1GenerateTypescriptTypesParams, reqEditors ...RequestEditorFn) (*V1GenerateTypescriptTypesResponse, error) - - // V1UpgradePostgresVersionWithBodyWithResponse request with any body - V1UpgradePostgresVersionWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpgradePostgresVersionResponse, error) - - V1UpgradePostgresVersionWithResponse(ctx context.Context, ref string, body V1UpgradePostgresVersionJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpgradePostgresVersionResponse, error) - - // V1GetPostgresUpgradeEligibilityWithResponse request - V1GetPostgresUpgradeEligibilityWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetPostgresUpgradeEligibilityResponse, error) - - // V1GetPostgresUpgradeStatusWithResponse request - V1GetPostgresUpgradeStatusWithResponse(ctx context.Context, ref string, params *V1GetPostgresUpgradeStatusParams, reqEditors ...RequestEditorFn) (*V1GetPostgresUpgradeStatusResponse, error) - - // V1DeactivateVanitySubdomainConfigWithResponse request - V1DeactivateVanitySubdomainConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1DeactivateVanitySubdomainConfigResponse, error) - - // V1GetVanitySubdomainConfigWithResponse request - V1GetVanitySubdomainConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetVanitySubdomainConfigResponse, error) - - // V1ActivateVanitySubdomainConfigWithBodyWithResponse request with any body - V1ActivateVanitySubdomainConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1ActivateVanitySubdomainConfigResponse, error) - - V1ActivateVanitySubdomainConfigWithResponse(ctx context.Context, ref string, body V1ActivateVanitySubdomainConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1ActivateVanitySubdomainConfigResponse, error) - - // V1CheckVanitySubdomainAvailabilityWithBodyWithResponse request with any body - V1CheckVanitySubdomainAvailabilityWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CheckVanitySubdomainAvailabilityResponse, error) - - V1CheckVanitySubdomainAvailabilityWithResponse(ctx context.Context, ref string, body V1CheckVanitySubdomainAvailabilityJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CheckVanitySubdomainAvailabilityResponse, error) - - // V1ListAllSnippetsWithResponse request - V1ListAllSnippetsWithResponse(ctx context.Context, params *V1ListAllSnippetsParams, reqEditors ...RequestEditorFn) (*V1ListAllSnippetsResponse, error) - - // V1GetASnippetWithResponse request - V1GetASnippetWithResponse(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*V1GetASnippetResponse, error) -} - -type V1DeleteABranchResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *BranchDeleteResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1DeleteABranchResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1DeleteABranchResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1DeleteABranchResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetABranchConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *BranchDetailResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetABranchConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetABranchConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetABranchConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdateABranchConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *BranchResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1UpdateABranchConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdateABranchConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdateABranchConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1DiffABranchResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1DiffABranchResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1DiffABranchResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1DiffABranchResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1MergeABranchResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *BranchUpdateResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1MergeABranchResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1MergeABranchResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1MergeABranchResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1PushABranchResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *BranchUpdateResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1PushABranchResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1PushABranchResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1PushABranchResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ResetABranchResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *BranchUpdateResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1ResetABranchResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ResetABranchResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ResetABranchResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1RestoreABranchResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *BranchRestoreResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1RestoreABranchResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1RestoreABranchResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1RestoreABranchResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1AuthorizeUserResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1AuthorizeUserResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1AuthorizeUserResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1AuthorizeUserResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1OauthAuthorizeProjectClaimResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1OauthAuthorizeProjectClaimResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1OauthAuthorizeProjectClaimResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1OauthAuthorizeProjectClaimResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1RevokeTokenResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1RevokeTokenResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1RevokeTokenResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1RevokeTokenResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ExchangeOauthTokenResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *OAuthTokenResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1ExchangeOauthTokenResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ExchangeOauthTokenResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ExchangeOauthTokenResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ListAllOrganizationsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *[]OrganizationResponseV1Output -} - -// Status returns HTTPResponse.Status -func (r V1ListAllOrganizationsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ListAllOrganizationsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ListAllOrganizationsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1CreateAnOrganizationResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *OrganizationResponseV1Output -} - -// Status returns HTTPResponse.Status -func (r V1CreateAnOrganizationResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1CreateAnOrganizationResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1CreateAnOrganizationResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetAnOrganizationResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *V1OrganizationSlugResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetAnOrganizationResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetAnOrganizationResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetAnOrganizationResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetOrganizationEntitlementsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *V1ListEntitlementsResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetOrganizationEntitlementsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetOrganizationEntitlementsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetOrganizationEntitlementsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ListOrganizationMembersResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *[]V1OrganizationMemberResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1ListOrganizationMembersResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ListOrganizationMembersResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ListOrganizationMembersResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetOrganizationProjectClaimResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *OrganizationProjectClaimResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetOrganizationProjectClaimResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetOrganizationProjectClaimResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetOrganizationProjectClaimResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ClaimProjectForOrganizationResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1ClaimProjectForOrganizationResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ClaimProjectForOrganizationResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ClaimProjectForOrganizationResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetAllProjectsForOrganizationResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *OrganizationProjectsResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetAllProjectsForOrganizationResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetAllProjectsForOrganizationResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetAllProjectsForOrganizationResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetProfileResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *V1ProfileResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetProfileResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetProfileResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetProfileResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ListAllProjectsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *[]V1ProjectWithDatabaseResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1ListAllProjectsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ListAllProjectsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ListAllProjectsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1CreateAProjectResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *V1ProjectResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1CreateAProjectResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1CreateAProjectResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1CreateAProjectResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetAvailableRegionsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *RegionsInfoV1Output -} - -// Status returns HTTPResponse.Status -func (r V1GetAvailableRegionsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetAvailableRegionsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetAvailableRegionsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1DeleteAProjectResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *V1ProjectRefResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1DeleteAProjectResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1DeleteAProjectResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1DeleteAProjectResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetProjectResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *V1ProjectWithDatabaseResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetProjectResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetProjectResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetProjectResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdateAProjectResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *V1ProjectRefResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1UpdateAProjectResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdateAProjectResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdateAProjectResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ListActionRunsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *ListActionRunResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1ListActionRunsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ListActionRunsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ListActionRunsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1CountActionRunsResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1CountActionRunsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1CountActionRunsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1CountActionRunsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetActionRunResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *ActionRunResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetActionRunResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetActionRunResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetActionRunResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetActionRunLogsResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1GetActionRunLogsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetActionRunLogsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetActionRunLogsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdateActionRunStatusResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *UpdateRunStatusResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1UpdateActionRunStatusResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdateActionRunStatusResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdateActionRunStatusResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetPerformanceAdvisorsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *V1ProjectAdvisorsResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetPerformanceAdvisorsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetPerformanceAdvisorsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetPerformanceAdvisorsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetSecurityAdvisorsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *V1ProjectAdvisorsResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetSecurityAdvisorsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetSecurityAdvisorsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetSecurityAdvisorsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetProjectFunctionCombinedStatsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *AnalyticsResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetProjectFunctionCombinedStatsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetProjectFunctionCombinedStatsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetProjectFunctionCombinedStatsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetProjectLogsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *AnalyticsResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetProjectLogsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetProjectLogsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetProjectLogsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetProjectLogsAllResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1GetProjectLogsAllResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetProjectLogsAllResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetProjectLogsAllResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ScrapeProjectMetricsResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1ScrapeProjectMetricsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ScrapeProjectMetricsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ScrapeProjectMetricsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetProjectUsageApiCountResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *V1GetUsageApiCountResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetProjectUsageApiCountResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetProjectUsageApiCountResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetProjectUsageApiCountResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetProjectUsageRequestCountResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *V1GetUsageApiRequestsCountResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetProjectUsageRequestCountResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetProjectUsageRequestCountResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetProjectUsageRequestCountResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetProjectApiKeysResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *[]ApiKeyResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetProjectApiKeysResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetProjectApiKeysResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetProjectApiKeysResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1CreateProjectApiKeyResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *ApiKeyResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1CreateProjectApiKeyResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1CreateProjectApiKeyResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1CreateProjectApiKeyResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetProjectLegacyApiKeysResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *LegacyApiKeysResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetProjectLegacyApiKeysResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetProjectLegacyApiKeysResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetProjectLegacyApiKeysResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdateProjectLegacyApiKeysResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *LegacyApiKeysResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1UpdateProjectLegacyApiKeysResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdateProjectLegacyApiKeysResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdateProjectLegacyApiKeysResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1DeleteProjectApiKeyResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *ApiKeyResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1DeleteProjectApiKeyResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1DeleteProjectApiKeyResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1DeleteProjectApiKeyResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetProjectApiKeyResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *ApiKeyResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetProjectApiKeyResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetProjectApiKeyResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetProjectApiKeyResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdateProjectApiKeyResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *ApiKeyResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1UpdateProjectApiKeyResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdateProjectApiKeyResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdateProjectApiKeyResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ListProjectAddonsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *ListProjectAddonsResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1ListProjectAddonsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ListProjectAddonsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ListProjectAddonsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ApplyProjectAddonResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1ApplyProjectAddonResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ApplyProjectAddonResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ApplyProjectAddonResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1RemoveProjectAddonResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1RemoveProjectAddonResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1RemoveProjectAddonResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1RemoveProjectAddonResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1DisablePreviewBranchingResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1DisablePreviewBranchingResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1DisablePreviewBranchingResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1DisablePreviewBranchingResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ListAllBranchesResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *[]BranchResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1ListAllBranchesResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ListAllBranchesResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ListAllBranchesResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1CreateABranchResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *BranchResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1CreateABranchResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1CreateABranchResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1CreateABranchResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetABranchResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *BranchResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetABranchResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetABranchResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetABranchResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1DeleteProjectClaimTokenResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1DeleteProjectClaimTokenResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1DeleteProjectClaimTokenResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1DeleteProjectClaimTokenResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetProjectClaimTokenResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *ProjectClaimTokenResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetProjectClaimTokenResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetProjectClaimTokenResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetProjectClaimTokenResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1CreateProjectClaimTokenResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *CreateProjectClaimTokenResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1CreateProjectClaimTokenResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1CreateProjectClaimTokenResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1CreateProjectClaimTokenResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1DeleteLoginRolesResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *DeleteRolesResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1DeleteLoginRolesResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1DeleteLoginRolesResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1DeleteLoginRolesResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1CreateLoginRoleResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *CreateRoleResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1CreateLoginRoleResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1CreateLoginRoleResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1CreateLoginRoleResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetAuthServiceConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *AuthConfigResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetAuthServiceConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetAuthServiceConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetAuthServiceConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdateAuthServiceConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *AuthConfigResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1UpdateAuthServiceConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdateAuthServiceConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdateAuthServiceConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetProjectSigningKeysResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *SigningKeysResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetProjectSigningKeysResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetProjectSigningKeysResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetProjectSigningKeysResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1CreateProjectSigningKeyResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *SigningKeyResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1CreateProjectSigningKeyResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1CreateProjectSigningKeyResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1CreateProjectSigningKeyResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetLegacySigningKeyResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *SigningKeyResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetLegacySigningKeyResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetLegacySigningKeyResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetLegacySigningKeyResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1CreateLegacySigningKeyResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *SigningKeyResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1CreateLegacySigningKeyResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1CreateLegacySigningKeyResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1CreateLegacySigningKeyResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1RemoveProjectSigningKeyResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *SigningKeyResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1RemoveProjectSigningKeyResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1RemoveProjectSigningKeyResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1RemoveProjectSigningKeyResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetProjectSigningKeyResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *SigningKeyResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetProjectSigningKeyResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetProjectSigningKeyResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetProjectSigningKeyResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdateProjectSigningKeyResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *SigningKeyResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1UpdateProjectSigningKeyResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdateProjectSigningKeyResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdateProjectSigningKeyResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ListAllSsoProviderResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *ListProvidersResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1ListAllSsoProviderResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ListAllSsoProviderResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ListAllSsoProviderResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1CreateASsoProviderResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *CreateProviderResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1CreateASsoProviderResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1CreateASsoProviderResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1CreateASsoProviderResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1DeleteASsoProviderResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *DeleteProviderResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1DeleteASsoProviderResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1DeleteASsoProviderResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1DeleteASsoProviderResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetASsoProviderResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *GetProviderResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetASsoProviderResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetASsoProviderResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetASsoProviderResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdateASsoProviderResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *UpdateProviderResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1UpdateASsoProviderResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdateASsoProviderResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdateASsoProviderResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ListProjectTpaIntegrationsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *[]ThirdPartyAuthOutput -} - -// Status returns HTTPResponse.Status -func (r V1ListProjectTpaIntegrationsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ListProjectTpaIntegrationsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ListProjectTpaIntegrationsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1CreateProjectTpaIntegrationResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *ThirdPartyAuthOutput -} - -// Status returns HTTPResponse.Status -func (r V1CreateProjectTpaIntegrationResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1CreateProjectTpaIntegrationResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1CreateProjectTpaIntegrationResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1DeleteProjectTpaIntegrationResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *ThirdPartyAuthOutput -} - -// Status returns HTTPResponse.Status -func (r V1DeleteProjectTpaIntegrationResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1DeleteProjectTpaIntegrationResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1DeleteProjectTpaIntegrationResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetProjectTpaIntegrationResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *ThirdPartyAuthOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetProjectTpaIntegrationResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetProjectTpaIntegrationResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetProjectTpaIntegrationResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetProjectPgbouncerConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *V1PgbouncerConfigResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetProjectPgbouncerConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetProjectPgbouncerConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetProjectPgbouncerConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetPoolerConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *[]SupavisorConfigResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetPoolerConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetPoolerConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetPoolerConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdatePoolerConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *UpdateSupavisorConfigResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1UpdatePoolerConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdatePoolerConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdatePoolerConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetPostgresConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *PostgresConfigResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetPostgresConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetPostgresConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetPostgresConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdatePostgresConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *PostgresConfigResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1UpdatePostgresConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdatePostgresConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdatePostgresConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetDatabaseDiskResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *DiskResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetDatabaseDiskResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetDatabaseDiskResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetDatabaseDiskResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ModifyDatabaseDiskResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1ModifyDatabaseDiskResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ModifyDatabaseDiskResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ModifyDatabaseDiskResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetProjectDiskAutoscaleConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *DiskAutoscaleConfigOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetProjectDiskAutoscaleConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetProjectDiskAutoscaleConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetProjectDiskAutoscaleConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetDiskUtilizationResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *DiskUtilMetricsResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetDiskUtilizationResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetDiskUtilizationResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetDiskUtilizationResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetRealtimeConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *RealtimeConfigResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetRealtimeConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetRealtimeConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetRealtimeConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdateRealtimeConfigResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1UpdateRealtimeConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdateRealtimeConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdateRealtimeConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ShutdownRealtimeResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1ShutdownRealtimeResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ShutdownRealtimeResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ShutdownRealtimeResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetStorageConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *StorageConfigResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetStorageConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetStorageConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetStorageConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdateStorageConfigResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1UpdateStorageConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdateStorageConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdateStorageConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1DeleteHostnameConfigResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1DeleteHostnameConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1DeleteHostnameConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1DeleteHostnameConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetHostnameConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *UpdateCustomHostnameResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetHostnameConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetHostnameConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetHostnameConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ActivateCustomHostnameResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *UpdateCustomHostnameResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1ActivateCustomHostnameResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ActivateCustomHostnameResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ActivateCustomHostnameResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdateHostnameConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *UpdateCustomHostnameResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1UpdateHostnameConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdateHostnameConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdateHostnameConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1VerifyDnsConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *UpdateCustomHostnameResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1VerifyDnsConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1VerifyDnsConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1VerifyDnsConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ListAllBackupsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *V1BackupsResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1ListAllBackupsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ListAllBackupsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ListAllBackupsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1RestorePhysicalBackupResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1RestorePhysicalBackupResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1RestorePhysicalBackupResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1RestorePhysicalBackupResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1RestorePitrBackupResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1RestorePitrBackupResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1RestorePitrBackupResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1RestorePitrBackupResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetRestorePointResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *V1RestorePointResponse -} - -// Status returns HTTPResponse.Status -func (r V1GetRestorePointResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetRestorePointResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetRestorePointResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1CreateRestorePointResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *V1RestorePointResponse -} - -// Status returns HTTPResponse.Status -func (r V1CreateRestorePointResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1CreateRestorePointResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1CreateRestorePointResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetBackupScheduleResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *V1BackupScheduleResponseOutput - JSON402 *PlanGateErrorBody -} - -// Status returns HTTPResponse.Status -func (r V1GetBackupScheduleResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetBackupScheduleResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetBackupScheduleResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdateBackupScheduleResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *V1BackupScheduleResponseOutput - JSON402 *PlanGateErrorBody -} - -// Status returns HTTPResponse.Status -func (r V1UpdateBackupScheduleResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdateBackupScheduleResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdateBackupScheduleResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UndoResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1UndoResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UndoResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UndoResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetDatabaseMetadataResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *GetProjectDbMetadataResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetDatabaseMetadataResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetDatabaseMetadataResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetDatabaseMetadataResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetJitAccessResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *JitAccessResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetJitAccessResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetJitAccessResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetJitAccessResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1AuthorizeJitAccessResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *JitAuthorizeAccessResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1AuthorizeJitAccessResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1AuthorizeJitAccessResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1AuthorizeJitAccessResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdateJitAccessResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *JitAccessResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1UpdateJitAccessResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdateJitAccessResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdateJitAccessResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1InviteExternalJitAccessResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *InviteExternalUserJitResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1InviteExternalJitAccessResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1InviteExternalJitAccessResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1InviteExternalJitAccessResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1AcceptInviteExternalJitAccessResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *JitAccessResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1AcceptInviteExternalJitAccessResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1AcceptInviteExternalJitAccessResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1AcceptInviteExternalJitAccessResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1DeleteInviteExternalJitAccessResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1DeleteInviteExternalJitAccessResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1DeleteInviteExternalJitAccessResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1DeleteInviteExternalJitAccessResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ListJitAccessResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *JitListAccessResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1ListJitAccessResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ListJitAccessResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ListJitAccessResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1DeleteJitAccessResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1DeleteJitAccessResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1DeleteJitAccessResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1DeleteJitAccessResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1RollbackMigrationsResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1RollbackMigrationsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1RollbackMigrationsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1RollbackMigrationsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ListMigrationHistoryResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *V1ListMigrationsResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1ListMigrationHistoryResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ListMigrationHistoryResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ListMigrationHistoryResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ApplyAMigrationResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1ApplyAMigrationResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ApplyAMigrationResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ApplyAMigrationResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpsertAMigrationResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1UpsertAMigrationResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpsertAMigrationResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpsertAMigrationResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetAMigrationResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *V1GetMigrationResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetAMigrationResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetAMigrationResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetAMigrationResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1PatchAMigrationResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1PatchAMigrationResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1PatchAMigrationResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1PatchAMigrationResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetDatabaseOpenapiResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *map[string]interface{} -} - -// Status returns HTTPResponse.Status -func (r V1GetDatabaseOpenapiResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetDatabaseOpenapiResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetDatabaseOpenapiResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdateDatabasePasswordResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *V1UpdatePasswordResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1UpdateDatabasePasswordResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdateDatabasePasswordResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdateDatabasePasswordResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1RunAQueryResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1RunAQueryResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1RunAQueryResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1RunAQueryResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ReadOnlyQueryResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1ReadOnlyQueryResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ReadOnlyQueryResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ReadOnlyQueryResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1EnableDatabaseWebhookResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1EnableDatabaseWebhookResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1EnableDatabaseWebhookResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1EnableDatabaseWebhookResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ListAllFunctionsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *[]FunctionResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1ListAllFunctionsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ListAllFunctionsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ListAllFunctionsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1CreateAFunctionResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *FunctionResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1CreateAFunctionResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1CreateAFunctionResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1CreateAFunctionResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1BulkUpdateFunctionsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *BulkUpdateFunctionResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1BulkUpdateFunctionsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1BulkUpdateFunctionsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1BulkUpdateFunctionsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1DeployAFunctionResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *DeployFunctionResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1DeployAFunctionResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1DeployAFunctionResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1DeployAFunctionResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1DeleteAFunctionResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1DeleteAFunctionResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1DeleteAFunctionResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1DeleteAFunctionResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetAFunctionResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *FunctionSlugResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetAFunctionResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetAFunctionResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetAFunctionResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdateAFunctionResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *FunctionSlugResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1UpdateAFunctionResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdateAFunctionResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdateAFunctionResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetAFunctionBodyResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *StreamableFile -} - -// Status returns HTTPResponse.Status -func (r V1GetAFunctionBodyResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetAFunctionBodyResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetAFunctionBodyResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetServicesHealthResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *[]V1ServiceHealthResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetServicesHealthResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetServicesHealthResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetServicesHealthResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetJitAccessConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *V1GetJitAccessConfig200JSONResponseBody -} - -// Status returns HTTPResponse.Status -func (r V1GetJitAccessConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetJitAccessConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetJitAccessConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdateJitAccessConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *V1UpdateJitAccessConfig200JSONResponseBody -} - -// Status returns HTTPResponse.Status -func (r V1UpdateJitAccessConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdateJitAccessConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdateJitAccessConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1DeleteNetworkBansResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1DeleteNetworkBansResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1DeleteNetworkBansResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1DeleteNetworkBansResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ListAllNetworkBansResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *NetworkBanResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1ListAllNetworkBansResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ListAllNetworkBansResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ListAllNetworkBansResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ListAllNetworkBansEnrichedResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *NetworkBanResponseEnrichedOutput -} - -// Status returns HTTPResponse.Status -func (r V1ListAllNetworkBansEnrichedResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ListAllNetworkBansEnrichedResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ListAllNetworkBansEnrichedResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetNetworkRestrictionsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *NetworkRestrictionsResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetNetworkRestrictionsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetNetworkRestrictionsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetNetworkRestrictionsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1PatchNetworkRestrictionsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *NetworkRestrictionsV2ResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1PatchNetworkRestrictionsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1PatchNetworkRestrictionsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1PatchNetworkRestrictionsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdateNetworkRestrictionsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *NetworkRestrictionsResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1UpdateNetworkRestrictionsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdateNetworkRestrictionsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdateNetworkRestrictionsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1PauseAProjectResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1PauseAProjectResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1PauseAProjectResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1PauseAProjectResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetPgsodiumConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *PgsodiumConfigResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetPgsodiumConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetPgsodiumConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetPgsodiumConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdatePgsodiumConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *PgsodiumConfigResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1UpdatePgsodiumConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdatePgsodiumConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdatePgsodiumConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetPostgrestServiceConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *PostgrestConfigWithJWTSecretResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetPostgrestServiceConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetPostgrestServiceConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetPostgrestServiceConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdatePostgrestServiceConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *V1PostgrestConfigResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1UpdatePostgrestServiceConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdatePostgrestServiceConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdatePostgrestServiceConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1RemoveAReadReplicaResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1RemoveAReadReplicaResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1RemoveAReadReplicaResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1RemoveAReadReplicaResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1SetupAReadReplicaResponse struct { - Body []byte - HTTPResponse *http.Response - JSON402 *PlanGateErrorBody -} - -// Status returns HTTPResponse.Status -func (r V1SetupAReadReplicaResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1SetupAReadReplicaResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1SetupAReadReplicaResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetReadonlyModeStatusResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *ReadOnlyStatusResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetReadonlyModeStatusResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetReadonlyModeStatusResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetReadonlyModeStatusResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1DisableReadonlyModeTemporarilyResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1DisableReadonlyModeTemporarilyResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1DisableReadonlyModeTemporarilyResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1DisableReadonlyModeTemporarilyResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1RestartAProjectResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1RestartAProjectResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1RestartAProjectResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1RestartAProjectResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ListAvailableRestoreVersionsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *GetProjectAvailableRestoreVersionsResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1ListAvailableRestoreVersionsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ListAvailableRestoreVersionsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ListAvailableRestoreVersionsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1RestoreAProjectResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1RestoreAProjectResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1RestoreAProjectResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1RestoreAProjectResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1CancelAProjectRestorationResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1CancelAProjectRestorationResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1CancelAProjectRestorationResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1CancelAProjectRestorationResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1BulkDeleteSecretsResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1BulkDeleteSecretsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1BulkDeleteSecretsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1BulkDeleteSecretsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ListAllSecretsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *[]SecretResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1ListAllSecretsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ListAllSecretsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ListAllSecretsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1BulkCreateSecretsResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1BulkCreateSecretsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1BulkCreateSecretsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1BulkCreateSecretsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetSslEnforcementConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *SslEnforcementResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetSslEnforcementConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetSslEnforcementConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetSslEnforcementConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpdateSslEnforcementConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *SslEnforcementResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1UpdateSslEnforcementConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpdateSslEnforcementConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpdateSslEnforcementConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ListAllBucketsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *[]V1StorageBucketResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1ListAllBucketsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ListAllBucketsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ListAllBucketsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GenerateTypescriptTypesResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *TypescriptResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GenerateTypescriptTypesResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GenerateTypescriptTypesResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GenerateTypescriptTypesResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1UpgradePostgresVersionResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *ProjectUpgradeInitiateResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1UpgradePostgresVersionResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1UpgradePostgresVersionResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1UpgradePostgresVersionResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetPostgresUpgradeEligibilityResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *ProjectUpgradeEligibilityResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetPostgresUpgradeEligibilityResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetPostgresUpgradeEligibilityResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetPostgresUpgradeEligibilityResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetPostgresUpgradeStatusResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *DatabaseUpgradeStatusResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetPostgresUpgradeStatusResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetPostgresUpgradeStatusResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetPostgresUpgradeStatusResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1DeactivateVanitySubdomainConfigResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r V1DeactivateVanitySubdomainConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1DeactivateVanitySubdomainConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1DeactivateVanitySubdomainConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetVanitySubdomainConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *VanitySubdomainConfigResponseOutput - JSON400 *PlanGateErrorBody -} - -// Status returns HTTPResponse.Status -func (r V1GetVanitySubdomainConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetVanitySubdomainConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetVanitySubdomainConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ActivateVanitySubdomainConfigResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *ActivateVanitySubdomainResponseOutput - JSON400 *PlanGateErrorBody -} - -// Status returns HTTPResponse.Status -func (r V1ActivateVanitySubdomainConfigResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ActivateVanitySubdomainConfigResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ActivateVanitySubdomainConfigResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1CheckVanitySubdomainAvailabilityResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *SubdomainAvailabilityResponseOutput - JSON400 *PlanGateErrorBody -} - -// Status returns HTTPResponse.Status -func (r V1CheckVanitySubdomainAvailabilityResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1CheckVanitySubdomainAvailabilityResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1CheckVanitySubdomainAvailabilityResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1ListAllSnippetsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *SnippetListOutput -} - -// Status returns HTTPResponse.Status -func (r V1ListAllSnippetsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1ListAllSnippetsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1ListAllSnippetsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type V1GetASnippetResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *SnippetResponseOutput -} - -// Status returns HTTPResponse.Status -func (r V1GetASnippetResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r V1GetASnippetResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r V1GetASnippetResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -// V1DeleteABranchWithResponse request returning *V1DeleteABranchResponse -func (c *ClientWithResponses) V1DeleteABranchWithResponse(ctx context.Context, branchIdOrRef string, params *V1DeleteABranchParams, reqEditors ...RequestEditorFn) (*V1DeleteABranchResponse, error) { - rsp, err := c.V1DeleteABranch(ctx, branchIdOrRef, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1DeleteABranchResponse(rsp) -} - -// V1GetABranchConfigWithResponse request returning *V1GetABranchConfigResponse -func (c *ClientWithResponses) V1GetABranchConfigWithResponse(ctx context.Context, branchIdOrRef string, reqEditors ...RequestEditorFn) (*V1GetABranchConfigResponse, error) { - rsp, err := c.V1GetABranchConfig(ctx, branchIdOrRef, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetABranchConfigResponse(rsp) -} - -// V1UpdateABranchConfigWithBodyWithResponse request with arbitrary body returning *V1UpdateABranchConfigResponse -func (c *ClientWithResponses) V1UpdateABranchConfigWithBodyWithResponse(ctx context.Context, branchIdOrRef string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateABranchConfigResponse, error) { - rsp, err := c.V1UpdateABranchConfigWithBody(ctx, branchIdOrRef, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateABranchConfigResponse(rsp) -} - -func (c *ClientWithResponses) V1UpdateABranchConfigWithResponse(ctx context.Context, branchIdOrRef string, body V1UpdateABranchConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateABranchConfigResponse, error) { - rsp, err := c.V1UpdateABranchConfig(ctx, branchIdOrRef, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateABranchConfigResponse(rsp) -} - -// V1DiffABranchWithResponse request returning *V1DiffABranchResponse -func (c *ClientWithResponses) V1DiffABranchWithResponse(ctx context.Context, branchIdOrRef string, params *V1DiffABranchParams, reqEditors ...RequestEditorFn) (*V1DiffABranchResponse, error) { - rsp, err := c.V1DiffABranch(ctx, branchIdOrRef, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1DiffABranchResponse(rsp) -} - -// V1MergeABranchWithBodyWithResponse request with arbitrary body returning *V1MergeABranchResponse -func (c *ClientWithResponses) V1MergeABranchWithBodyWithResponse(ctx context.Context, branchIdOrRef string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1MergeABranchResponse, error) { - rsp, err := c.V1MergeABranchWithBody(ctx, branchIdOrRef, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1MergeABranchResponse(rsp) -} - -func (c *ClientWithResponses) V1MergeABranchWithResponse(ctx context.Context, branchIdOrRef string, body V1MergeABranchJSONRequestBody, reqEditors ...RequestEditorFn) (*V1MergeABranchResponse, error) { - rsp, err := c.V1MergeABranch(ctx, branchIdOrRef, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1MergeABranchResponse(rsp) -} - -// V1PushABranchWithBodyWithResponse request with arbitrary body returning *V1PushABranchResponse -func (c *ClientWithResponses) V1PushABranchWithBodyWithResponse(ctx context.Context, branchIdOrRef string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1PushABranchResponse, error) { - rsp, err := c.V1PushABranchWithBody(ctx, branchIdOrRef, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1PushABranchResponse(rsp) -} - -func (c *ClientWithResponses) V1PushABranchWithResponse(ctx context.Context, branchIdOrRef string, body V1PushABranchJSONRequestBody, reqEditors ...RequestEditorFn) (*V1PushABranchResponse, error) { - rsp, err := c.V1PushABranch(ctx, branchIdOrRef, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1PushABranchResponse(rsp) -} - -// V1ResetABranchWithBodyWithResponse request with arbitrary body returning *V1ResetABranchResponse -func (c *ClientWithResponses) V1ResetABranchWithBodyWithResponse(ctx context.Context, branchIdOrRef string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1ResetABranchResponse, error) { - rsp, err := c.V1ResetABranchWithBody(ctx, branchIdOrRef, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ResetABranchResponse(rsp) -} - -func (c *ClientWithResponses) V1ResetABranchWithResponse(ctx context.Context, branchIdOrRef string, body V1ResetABranchJSONRequestBody, reqEditors ...RequestEditorFn) (*V1ResetABranchResponse, error) { - rsp, err := c.V1ResetABranch(ctx, branchIdOrRef, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ResetABranchResponse(rsp) -} - -// V1RestoreABranchWithResponse request returning *V1RestoreABranchResponse -func (c *ClientWithResponses) V1RestoreABranchWithResponse(ctx context.Context, branchIdOrRef string, reqEditors ...RequestEditorFn) (*V1RestoreABranchResponse, error) { - rsp, err := c.V1RestoreABranch(ctx, branchIdOrRef, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1RestoreABranchResponse(rsp) -} - -// V1AuthorizeUserWithResponse request returning *V1AuthorizeUserResponse -func (c *ClientWithResponses) V1AuthorizeUserWithResponse(ctx context.Context, params *V1AuthorizeUserParams, reqEditors ...RequestEditorFn) (*V1AuthorizeUserResponse, error) { - rsp, err := c.V1AuthorizeUser(ctx, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1AuthorizeUserResponse(rsp) -} - -// V1OauthAuthorizeProjectClaimWithResponse request returning *V1OauthAuthorizeProjectClaimResponse -func (c *ClientWithResponses) V1OauthAuthorizeProjectClaimWithResponse(ctx context.Context, params *V1OauthAuthorizeProjectClaimParams, reqEditors ...RequestEditorFn) (*V1OauthAuthorizeProjectClaimResponse, error) { - rsp, err := c.V1OauthAuthorizeProjectClaim(ctx, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1OauthAuthorizeProjectClaimResponse(rsp) -} - -// V1RevokeTokenWithBodyWithResponse request with arbitrary body returning *V1RevokeTokenResponse -func (c *ClientWithResponses) V1RevokeTokenWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1RevokeTokenResponse, error) { - rsp, err := c.V1RevokeTokenWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1RevokeTokenResponse(rsp) -} - -func (c *ClientWithResponses) V1RevokeTokenWithResponse(ctx context.Context, body V1RevokeTokenJSONRequestBody, reqEditors ...RequestEditorFn) (*V1RevokeTokenResponse, error) { - rsp, err := c.V1RevokeToken(ctx, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1RevokeTokenResponse(rsp) -} - -// V1ExchangeOauthTokenWithBodyWithResponse request with arbitrary body returning *V1ExchangeOauthTokenResponse -func (c *ClientWithResponses) V1ExchangeOauthTokenWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1ExchangeOauthTokenResponse, error) { - rsp, err := c.V1ExchangeOauthTokenWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ExchangeOauthTokenResponse(rsp) -} - -func (c *ClientWithResponses) V1ExchangeOauthTokenWithFormdataBodyWithResponse(ctx context.Context, body V1ExchangeOauthTokenFormdataRequestBody, reqEditors ...RequestEditorFn) (*V1ExchangeOauthTokenResponse, error) { - rsp, err := c.V1ExchangeOauthTokenWithFormdataBody(ctx, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ExchangeOauthTokenResponse(rsp) -} - -// V1ListAllOrganizationsWithResponse request returning *V1ListAllOrganizationsResponse -func (c *ClientWithResponses) V1ListAllOrganizationsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*V1ListAllOrganizationsResponse, error) { - rsp, err := c.V1ListAllOrganizations(ctx, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ListAllOrganizationsResponse(rsp) -} - -// V1CreateAnOrganizationWithBodyWithResponse request with arbitrary body returning *V1CreateAnOrganizationResponse -func (c *ClientWithResponses) V1CreateAnOrganizationWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CreateAnOrganizationResponse, error) { - rsp, err := c.V1CreateAnOrganizationWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateAnOrganizationResponse(rsp) -} - -func (c *ClientWithResponses) V1CreateAnOrganizationWithResponse(ctx context.Context, body V1CreateAnOrganizationJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CreateAnOrganizationResponse, error) { - rsp, err := c.V1CreateAnOrganization(ctx, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateAnOrganizationResponse(rsp) -} - -// V1GetAnOrganizationWithResponse request returning *V1GetAnOrganizationResponse -func (c *ClientWithResponses) V1GetAnOrganizationWithResponse(ctx context.Context, slug string, reqEditors ...RequestEditorFn) (*V1GetAnOrganizationResponse, error) { - rsp, err := c.V1GetAnOrganization(ctx, slug, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetAnOrganizationResponse(rsp) -} - -// V1GetOrganizationEntitlementsWithResponse request returning *V1GetOrganizationEntitlementsResponse -func (c *ClientWithResponses) V1GetOrganizationEntitlementsWithResponse(ctx context.Context, slug string, reqEditors ...RequestEditorFn) (*V1GetOrganizationEntitlementsResponse, error) { - rsp, err := c.V1GetOrganizationEntitlements(ctx, slug, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetOrganizationEntitlementsResponse(rsp) -} - -// V1ListOrganizationMembersWithResponse request returning *V1ListOrganizationMembersResponse -func (c *ClientWithResponses) V1ListOrganizationMembersWithResponse(ctx context.Context, slug string, reqEditors ...RequestEditorFn) (*V1ListOrganizationMembersResponse, error) { - rsp, err := c.V1ListOrganizationMembers(ctx, slug, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ListOrganizationMembersResponse(rsp) -} - -// V1GetOrganizationProjectClaimWithResponse request returning *V1GetOrganizationProjectClaimResponse -func (c *ClientWithResponses) V1GetOrganizationProjectClaimWithResponse(ctx context.Context, slug string, token string, reqEditors ...RequestEditorFn) (*V1GetOrganizationProjectClaimResponse, error) { - rsp, err := c.V1GetOrganizationProjectClaim(ctx, slug, token, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetOrganizationProjectClaimResponse(rsp) -} - -// V1ClaimProjectForOrganizationWithResponse request returning *V1ClaimProjectForOrganizationResponse -func (c *ClientWithResponses) V1ClaimProjectForOrganizationWithResponse(ctx context.Context, slug string, token string, reqEditors ...RequestEditorFn) (*V1ClaimProjectForOrganizationResponse, error) { - rsp, err := c.V1ClaimProjectForOrganization(ctx, slug, token, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ClaimProjectForOrganizationResponse(rsp) -} - -// V1GetAllProjectsForOrganizationWithResponse request returning *V1GetAllProjectsForOrganizationResponse -func (c *ClientWithResponses) V1GetAllProjectsForOrganizationWithResponse(ctx context.Context, slug string, params *V1GetAllProjectsForOrganizationParams, reqEditors ...RequestEditorFn) (*V1GetAllProjectsForOrganizationResponse, error) { - rsp, err := c.V1GetAllProjectsForOrganization(ctx, slug, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetAllProjectsForOrganizationResponse(rsp) -} - -// V1GetProfileWithResponse request returning *V1GetProfileResponse -func (c *ClientWithResponses) V1GetProfileWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*V1GetProfileResponse, error) { - rsp, err := c.V1GetProfile(ctx, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetProfileResponse(rsp) -} - -// V1ListAllProjectsWithResponse request returning *V1ListAllProjectsResponse -func (c *ClientWithResponses) V1ListAllProjectsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*V1ListAllProjectsResponse, error) { - rsp, err := c.V1ListAllProjects(ctx, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ListAllProjectsResponse(rsp) -} - -// V1CreateAProjectWithBodyWithResponse request with arbitrary body returning *V1CreateAProjectResponse -func (c *ClientWithResponses) V1CreateAProjectWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CreateAProjectResponse, error) { - rsp, err := c.V1CreateAProjectWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateAProjectResponse(rsp) -} - -func (c *ClientWithResponses) V1CreateAProjectWithResponse(ctx context.Context, body V1CreateAProjectJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CreateAProjectResponse, error) { - rsp, err := c.V1CreateAProject(ctx, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateAProjectResponse(rsp) -} - -// V1GetAvailableRegionsWithResponse request returning *V1GetAvailableRegionsResponse -func (c *ClientWithResponses) V1GetAvailableRegionsWithResponse(ctx context.Context, params *V1GetAvailableRegionsParams, reqEditors ...RequestEditorFn) (*V1GetAvailableRegionsResponse, error) { - rsp, err := c.V1GetAvailableRegions(ctx, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetAvailableRegionsResponse(rsp) -} - -// V1DeleteAProjectWithResponse request returning *V1DeleteAProjectResponse -func (c *ClientWithResponses) V1DeleteAProjectWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1DeleteAProjectResponse, error) { - rsp, err := c.V1DeleteAProject(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1DeleteAProjectResponse(rsp) -} - -// V1GetProjectWithResponse request returning *V1GetProjectResponse -func (c *ClientWithResponses) V1GetProjectWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetProjectResponse, error) { - rsp, err := c.V1GetProject(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetProjectResponse(rsp) -} - -// V1UpdateAProjectWithBodyWithResponse request with arbitrary body returning *V1UpdateAProjectResponse -func (c *ClientWithResponses) V1UpdateAProjectWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateAProjectResponse, error) { - rsp, err := c.V1UpdateAProjectWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateAProjectResponse(rsp) -} - -func (c *ClientWithResponses) V1UpdateAProjectWithResponse(ctx context.Context, ref string, body V1UpdateAProjectJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateAProjectResponse, error) { - rsp, err := c.V1UpdateAProject(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateAProjectResponse(rsp) -} - -// V1ListActionRunsWithResponse request returning *V1ListActionRunsResponse -func (c *ClientWithResponses) V1ListActionRunsWithResponse(ctx context.Context, ref string, params *V1ListActionRunsParams, reqEditors ...RequestEditorFn) (*V1ListActionRunsResponse, error) { - rsp, err := c.V1ListActionRuns(ctx, ref, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ListActionRunsResponse(rsp) -} - -// V1CountActionRunsWithResponse request returning *V1CountActionRunsResponse -func (c *ClientWithResponses) V1CountActionRunsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1CountActionRunsResponse, error) { - rsp, err := c.V1CountActionRuns(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CountActionRunsResponse(rsp) -} - -// V1GetActionRunWithResponse request returning *V1GetActionRunResponse -func (c *ClientWithResponses) V1GetActionRunWithResponse(ctx context.Context, ref string, runId string, reqEditors ...RequestEditorFn) (*V1GetActionRunResponse, error) { - rsp, err := c.V1GetActionRun(ctx, ref, runId, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetActionRunResponse(rsp) -} - -// V1GetActionRunLogsWithResponse request returning *V1GetActionRunLogsResponse -func (c *ClientWithResponses) V1GetActionRunLogsWithResponse(ctx context.Context, ref string, runId string, reqEditors ...RequestEditorFn) (*V1GetActionRunLogsResponse, error) { - rsp, err := c.V1GetActionRunLogs(ctx, ref, runId, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetActionRunLogsResponse(rsp) -} - -// V1UpdateActionRunStatusWithBodyWithResponse request with arbitrary body returning *V1UpdateActionRunStatusResponse -func (c *ClientWithResponses) V1UpdateActionRunStatusWithBodyWithResponse(ctx context.Context, ref string, runId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateActionRunStatusResponse, error) { - rsp, err := c.V1UpdateActionRunStatusWithBody(ctx, ref, runId, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateActionRunStatusResponse(rsp) -} - -func (c *ClientWithResponses) V1UpdateActionRunStatusWithResponse(ctx context.Context, ref string, runId string, body V1UpdateActionRunStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateActionRunStatusResponse, error) { - rsp, err := c.V1UpdateActionRunStatus(ctx, ref, runId, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateActionRunStatusResponse(rsp) -} - -// V1GetPerformanceAdvisorsWithResponse request returning *V1GetPerformanceAdvisorsResponse -func (c *ClientWithResponses) V1GetPerformanceAdvisorsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetPerformanceAdvisorsResponse, error) { - rsp, err := c.V1GetPerformanceAdvisors(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetPerformanceAdvisorsResponse(rsp) -} - -// V1GetSecurityAdvisorsWithResponse request returning *V1GetSecurityAdvisorsResponse -func (c *ClientWithResponses) V1GetSecurityAdvisorsWithResponse(ctx context.Context, ref string, params *V1GetSecurityAdvisorsParams, reqEditors ...RequestEditorFn) (*V1GetSecurityAdvisorsResponse, error) { - rsp, err := c.V1GetSecurityAdvisors(ctx, ref, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetSecurityAdvisorsResponse(rsp) -} - -// V1GetProjectFunctionCombinedStatsWithResponse request returning *V1GetProjectFunctionCombinedStatsResponse -func (c *ClientWithResponses) V1GetProjectFunctionCombinedStatsWithResponse(ctx context.Context, ref string, params *V1GetProjectFunctionCombinedStatsParams, reqEditors ...RequestEditorFn) (*V1GetProjectFunctionCombinedStatsResponse, error) { - rsp, err := c.V1GetProjectFunctionCombinedStats(ctx, ref, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetProjectFunctionCombinedStatsResponse(rsp) -} - -// V1GetProjectLogsWithResponse request returning *V1GetProjectLogsResponse -func (c *ClientWithResponses) V1GetProjectLogsWithResponse(ctx context.Context, ref string, params *V1GetProjectLogsParams, reqEditors ...RequestEditorFn) (*V1GetProjectLogsResponse, error) { - rsp, err := c.V1GetProjectLogs(ctx, ref, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetProjectLogsResponse(rsp) -} - -// V1GetProjectLogsAllWithResponse request returning *V1GetProjectLogsAllResponse -func (c *ClientWithResponses) V1GetProjectLogsAllWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetProjectLogsAllResponse, error) { - rsp, err := c.V1GetProjectLogsAll(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetProjectLogsAllResponse(rsp) -} - -// V1ScrapeProjectMetricsWithResponse request returning *V1ScrapeProjectMetricsResponse -func (c *ClientWithResponses) V1ScrapeProjectMetricsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ScrapeProjectMetricsResponse, error) { - rsp, err := c.V1ScrapeProjectMetrics(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ScrapeProjectMetricsResponse(rsp) -} - -// V1GetProjectUsageApiCountWithResponse request returning *V1GetProjectUsageApiCountResponse -func (c *ClientWithResponses) V1GetProjectUsageApiCountWithResponse(ctx context.Context, ref string, params *V1GetProjectUsageApiCountParams, reqEditors ...RequestEditorFn) (*V1GetProjectUsageApiCountResponse, error) { - rsp, err := c.V1GetProjectUsageApiCount(ctx, ref, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetProjectUsageApiCountResponse(rsp) -} - -// V1GetProjectUsageRequestCountWithResponse request returning *V1GetProjectUsageRequestCountResponse -func (c *ClientWithResponses) V1GetProjectUsageRequestCountWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetProjectUsageRequestCountResponse, error) { - rsp, err := c.V1GetProjectUsageRequestCount(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetProjectUsageRequestCountResponse(rsp) -} - -// V1GetProjectApiKeysWithResponse request returning *V1GetProjectApiKeysResponse -func (c *ClientWithResponses) V1GetProjectApiKeysWithResponse(ctx context.Context, ref string, params *V1GetProjectApiKeysParams, reqEditors ...RequestEditorFn) (*V1GetProjectApiKeysResponse, error) { - rsp, err := c.V1GetProjectApiKeys(ctx, ref, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetProjectApiKeysResponse(rsp) -} - -// V1CreateProjectApiKeyWithBodyWithResponse request with arbitrary body returning *V1CreateProjectApiKeyResponse -func (c *ClientWithResponses) V1CreateProjectApiKeyWithBodyWithResponse(ctx context.Context, ref string, params *V1CreateProjectApiKeyParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CreateProjectApiKeyResponse, error) { - rsp, err := c.V1CreateProjectApiKeyWithBody(ctx, ref, params, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateProjectApiKeyResponse(rsp) -} - -func (c *ClientWithResponses) V1CreateProjectApiKeyWithResponse(ctx context.Context, ref string, params *V1CreateProjectApiKeyParams, body V1CreateProjectApiKeyJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CreateProjectApiKeyResponse, error) { - rsp, err := c.V1CreateProjectApiKey(ctx, ref, params, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateProjectApiKeyResponse(rsp) -} - -// V1GetProjectLegacyApiKeysWithResponse request returning *V1GetProjectLegacyApiKeysResponse -func (c *ClientWithResponses) V1GetProjectLegacyApiKeysWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetProjectLegacyApiKeysResponse, error) { - rsp, err := c.V1GetProjectLegacyApiKeys(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetProjectLegacyApiKeysResponse(rsp) -} - -// V1UpdateProjectLegacyApiKeysWithResponse request returning *V1UpdateProjectLegacyApiKeysResponse -func (c *ClientWithResponses) V1UpdateProjectLegacyApiKeysWithResponse(ctx context.Context, ref string, params *V1UpdateProjectLegacyApiKeysParams, reqEditors ...RequestEditorFn) (*V1UpdateProjectLegacyApiKeysResponse, error) { - rsp, err := c.V1UpdateProjectLegacyApiKeys(ctx, ref, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateProjectLegacyApiKeysResponse(rsp) -} - -// V1DeleteProjectApiKeyWithResponse request returning *V1DeleteProjectApiKeyResponse -func (c *ClientWithResponses) V1DeleteProjectApiKeyWithResponse(ctx context.Context, ref string, id openapi_types.UUID, params *V1DeleteProjectApiKeyParams, reqEditors ...RequestEditorFn) (*V1DeleteProjectApiKeyResponse, error) { - rsp, err := c.V1DeleteProjectApiKey(ctx, ref, id, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1DeleteProjectApiKeyResponse(rsp) -} - -// V1GetProjectApiKeyWithResponse request returning *V1GetProjectApiKeyResponse -func (c *ClientWithResponses) V1GetProjectApiKeyWithResponse(ctx context.Context, ref string, id openapi_types.UUID, params *V1GetProjectApiKeyParams, reqEditors ...RequestEditorFn) (*V1GetProjectApiKeyResponse, error) { - rsp, err := c.V1GetProjectApiKey(ctx, ref, id, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetProjectApiKeyResponse(rsp) -} - -// V1UpdateProjectApiKeyWithBodyWithResponse request with arbitrary body returning *V1UpdateProjectApiKeyResponse -func (c *ClientWithResponses) V1UpdateProjectApiKeyWithBodyWithResponse(ctx context.Context, ref string, id openapi_types.UUID, params *V1UpdateProjectApiKeyParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateProjectApiKeyResponse, error) { - rsp, err := c.V1UpdateProjectApiKeyWithBody(ctx, ref, id, params, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateProjectApiKeyResponse(rsp) -} - -func (c *ClientWithResponses) V1UpdateProjectApiKeyWithResponse(ctx context.Context, ref string, id openapi_types.UUID, params *V1UpdateProjectApiKeyParams, body V1UpdateProjectApiKeyJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateProjectApiKeyResponse, error) { - rsp, err := c.V1UpdateProjectApiKey(ctx, ref, id, params, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateProjectApiKeyResponse(rsp) -} - -// V1ListProjectAddonsWithResponse request returning *V1ListProjectAddonsResponse -func (c *ClientWithResponses) V1ListProjectAddonsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListProjectAddonsResponse, error) { - rsp, err := c.V1ListProjectAddons(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ListProjectAddonsResponse(rsp) -} - -// V1ApplyProjectAddonWithBodyWithResponse request with arbitrary body returning *V1ApplyProjectAddonResponse -func (c *ClientWithResponses) V1ApplyProjectAddonWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1ApplyProjectAddonResponse, error) { - rsp, err := c.V1ApplyProjectAddonWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ApplyProjectAddonResponse(rsp) -} - -func (c *ClientWithResponses) V1ApplyProjectAddonWithResponse(ctx context.Context, ref string, body V1ApplyProjectAddonJSONRequestBody, reqEditors ...RequestEditorFn) (*V1ApplyProjectAddonResponse, error) { - rsp, err := c.V1ApplyProjectAddon(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ApplyProjectAddonResponse(rsp) -} - -// V1RemoveProjectAddonWithResponse request returning *V1RemoveProjectAddonResponse -func (c *ClientWithResponses) V1RemoveProjectAddonWithResponse(ctx context.Context, ref string, addonVariant struct { - union json.RawMessage -}, reqEditors ...RequestEditorFn) (*V1RemoveProjectAddonResponse, error) { - rsp, err := c.V1RemoveProjectAddon(ctx, ref, addonVariant, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1RemoveProjectAddonResponse(rsp) -} - -// V1DisablePreviewBranchingWithResponse request returning *V1DisablePreviewBranchingResponse -func (c *ClientWithResponses) V1DisablePreviewBranchingWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1DisablePreviewBranchingResponse, error) { - rsp, err := c.V1DisablePreviewBranching(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1DisablePreviewBranchingResponse(rsp) -} - -// V1ListAllBranchesWithResponse request returning *V1ListAllBranchesResponse -func (c *ClientWithResponses) V1ListAllBranchesWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListAllBranchesResponse, error) { - rsp, err := c.V1ListAllBranches(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ListAllBranchesResponse(rsp) -} - -// V1CreateABranchWithBodyWithResponse request with arbitrary body returning *V1CreateABranchResponse -func (c *ClientWithResponses) V1CreateABranchWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CreateABranchResponse, error) { - rsp, err := c.V1CreateABranchWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateABranchResponse(rsp) -} - -func (c *ClientWithResponses) V1CreateABranchWithResponse(ctx context.Context, ref string, body V1CreateABranchJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CreateABranchResponse, error) { - rsp, err := c.V1CreateABranch(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateABranchResponse(rsp) -} - -// V1GetABranchWithResponse request returning *V1GetABranchResponse -func (c *ClientWithResponses) V1GetABranchWithResponse(ctx context.Context, ref string, name string, reqEditors ...RequestEditorFn) (*V1GetABranchResponse, error) { - rsp, err := c.V1GetABranch(ctx, ref, name, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetABranchResponse(rsp) -} - -// V1DeleteProjectClaimTokenWithResponse request returning *V1DeleteProjectClaimTokenResponse -func (c *ClientWithResponses) V1DeleteProjectClaimTokenWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1DeleteProjectClaimTokenResponse, error) { - rsp, err := c.V1DeleteProjectClaimToken(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1DeleteProjectClaimTokenResponse(rsp) -} - -// V1GetProjectClaimTokenWithResponse request returning *V1GetProjectClaimTokenResponse -func (c *ClientWithResponses) V1GetProjectClaimTokenWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetProjectClaimTokenResponse, error) { - rsp, err := c.V1GetProjectClaimToken(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetProjectClaimTokenResponse(rsp) -} - -// V1CreateProjectClaimTokenWithResponse request returning *V1CreateProjectClaimTokenResponse -func (c *ClientWithResponses) V1CreateProjectClaimTokenWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1CreateProjectClaimTokenResponse, error) { - rsp, err := c.V1CreateProjectClaimToken(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateProjectClaimTokenResponse(rsp) -} - -// V1DeleteLoginRolesWithResponse request returning *V1DeleteLoginRolesResponse -func (c *ClientWithResponses) V1DeleteLoginRolesWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1DeleteLoginRolesResponse, error) { - rsp, err := c.V1DeleteLoginRoles(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1DeleteLoginRolesResponse(rsp) -} - -// V1CreateLoginRoleWithBodyWithResponse request with arbitrary body returning *V1CreateLoginRoleResponse -func (c *ClientWithResponses) V1CreateLoginRoleWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CreateLoginRoleResponse, error) { - rsp, err := c.V1CreateLoginRoleWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateLoginRoleResponse(rsp) -} - -func (c *ClientWithResponses) V1CreateLoginRoleWithResponse(ctx context.Context, ref string, body V1CreateLoginRoleJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CreateLoginRoleResponse, error) { - rsp, err := c.V1CreateLoginRole(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateLoginRoleResponse(rsp) -} - -// V1GetAuthServiceConfigWithResponse request returning *V1GetAuthServiceConfigResponse -func (c *ClientWithResponses) V1GetAuthServiceConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetAuthServiceConfigResponse, error) { - rsp, err := c.V1GetAuthServiceConfig(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetAuthServiceConfigResponse(rsp) -} - -// V1UpdateAuthServiceConfigWithBodyWithResponse request with arbitrary body returning *V1UpdateAuthServiceConfigResponse -func (c *ClientWithResponses) V1UpdateAuthServiceConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateAuthServiceConfigResponse, error) { - rsp, err := c.V1UpdateAuthServiceConfigWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateAuthServiceConfigResponse(rsp) -} - -func (c *ClientWithResponses) V1UpdateAuthServiceConfigWithResponse(ctx context.Context, ref string, body V1UpdateAuthServiceConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateAuthServiceConfigResponse, error) { - rsp, err := c.V1UpdateAuthServiceConfig(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateAuthServiceConfigResponse(rsp) -} - -// V1GetProjectSigningKeysWithResponse request returning *V1GetProjectSigningKeysResponse -func (c *ClientWithResponses) V1GetProjectSigningKeysWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetProjectSigningKeysResponse, error) { - rsp, err := c.V1GetProjectSigningKeys(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetProjectSigningKeysResponse(rsp) -} - -// V1CreateProjectSigningKeyWithBodyWithResponse request with arbitrary body returning *V1CreateProjectSigningKeyResponse -func (c *ClientWithResponses) V1CreateProjectSigningKeyWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CreateProjectSigningKeyResponse, error) { - rsp, err := c.V1CreateProjectSigningKeyWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateProjectSigningKeyResponse(rsp) -} - -func (c *ClientWithResponses) V1CreateProjectSigningKeyWithResponse(ctx context.Context, ref string, body V1CreateProjectSigningKeyJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CreateProjectSigningKeyResponse, error) { - rsp, err := c.V1CreateProjectSigningKey(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateProjectSigningKeyResponse(rsp) -} - -// V1GetLegacySigningKeyWithResponse request returning *V1GetLegacySigningKeyResponse -func (c *ClientWithResponses) V1GetLegacySigningKeyWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetLegacySigningKeyResponse, error) { - rsp, err := c.V1GetLegacySigningKey(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetLegacySigningKeyResponse(rsp) -} - -// V1CreateLegacySigningKeyWithResponse request returning *V1CreateLegacySigningKeyResponse -func (c *ClientWithResponses) V1CreateLegacySigningKeyWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1CreateLegacySigningKeyResponse, error) { - rsp, err := c.V1CreateLegacySigningKey(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateLegacySigningKeyResponse(rsp) -} - -// V1RemoveProjectSigningKeyWithResponse request returning *V1RemoveProjectSigningKeyResponse -func (c *ClientWithResponses) V1RemoveProjectSigningKeyWithResponse(ctx context.Context, ref string, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*V1RemoveProjectSigningKeyResponse, error) { - rsp, err := c.V1RemoveProjectSigningKey(ctx, ref, id, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1RemoveProjectSigningKeyResponse(rsp) -} - -// V1GetProjectSigningKeyWithResponse request returning *V1GetProjectSigningKeyResponse -func (c *ClientWithResponses) V1GetProjectSigningKeyWithResponse(ctx context.Context, ref string, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*V1GetProjectSigningKeyResponse, error) { - rsp, err := c.V1GetProjectSigningKey(ctx, ref, id, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetProjectSigningKeyResponse(rsp) -} - -// V1UpdateProjectSigningKeyWithBodyWithResponse request with arbitrary body returning *V1UpdateProjectSigningKeyResponse -func (c *ClientWithResponses) V1UpdateProjectSigningKeyWithBodyWithResponse(ctx context.Context, ref string, id openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateProjectSigningKeyResponse, error) { - rsp, err := c.V1UpdateProjectSigningKeyWithBody(ctx, ref, id, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateProjectSigningKeyResponse(rsp) -} - -func (c *ClientWithResponses) V1UpdateProjectSigningKeyWithResponse(ctx context.Context, ref string, id openapi_types.UUID, body V1UpdateProjectSigningKeyJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateProjectSigningKeyResponse, error) { - rsp, err := c.V1UpdateProjectSigningKey(ctx, ref, id, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateProjectSigningKeyResponse(rsp) -} - -// V1ListAllSsoProviderWithResponse request returning *V1ListAllSsoProviderResponse -func (c *ClientWithResponses) V1ListAllSsoProviderWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListAllSsoProviderResponse, error) { - rsp, err := c.V1ListAllSsoProvider(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ListAllSsoProviderResponse(rsp) -} - -// V1CreateASsoProviderWithBodyWithResponse request with arbitrary body returning *V1CreateASsoProviderResponse -func (c *ClientWithResponses) V1CreateASsoProviderWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CreateASsoProviderResponse, error) { - rsp, err := c.V1CreateASsoProviderWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateASsoProviderResponse(rsp) -} - -func (c *ClientWithResponses) V1CreateASsoProviderWithResponse(ctx context.Context, ref string, body V1CreateASsoProviderJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CreateASsoProviderResponse, error) { - rsp, err := c.V1CreateASsoProvider(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateASsoProviderResponse(rsp) -} - -// V1DeleteASsoProviderWithResponse request returning *V1DeleteASsoProviderResponse -func (c *ClientWithResponses) V1DeleteASsoProviderWithResponse(ctx context.Context, ref string, providerId openapi_types.UUID, reqEditors ...RequestEditorFn) (*V1DeleteASsoProviderResponse, error) { - rsp, err := c.V1DeleteASsoProvider(ctx, ref, providerId, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1DeleteASsoProviderResponse(rsp) -} - -// V1GetASsoProviderWithResponse request returning *V1GetASsoProviderResponse -func (c *ClientWithResponses) V1GetASsoProviderWithResponse(ctx context.Context, ref string, providerId openapi_types.UUID, reqEditors ...RequestEditorFn) (*V1GetASsoProviderResponse, error) { - rsp, err := c.V1GetASsoProvider(ctx, ref, providerId, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetASsoProviderResponse(rsp) -} - -// V1UpdateASsoProviderWithBodyWithResponse request with arbitrary body returning *V1UpdateASsoProviderResponse -func (c *ClientWithResponses) V1UpdateASsoProviderWithBodyWithResponse(ctx context.Context, ref string, providerId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateASsoProviderResponse, error) { - rsp, err := c.V1UpdateASsoProviderWithBody(ctx, ref, providerId, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateASsoProviderResponse(rsp) -} - -func (c *ClientWithResponses) V1UpdateASsoProviderWithResponse(ctx context.Context, ref string, providerId openapi_types.UUID, body V1UpdateASsoProviderJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateASsoProviderResponse, error) { - rsp, err := c.V1UpdateASsoProvider(ctx, ref, providerId, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateASsoProviderResponse(rsp) -} - -// V1ListProjectTpaIntegrationsWithResponse request returning *V1ListProjectTpaIntegrationsResponse -func (c *ClientWithResponses) V1ListProjectTpaIntegrationsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListProjectTpaIntegrationsResponse, error) { - rsp, err := c.V1ListProjectTpaIntegrations(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ListProjectTpaIntegrationsResponse(rsp) -} - -// V1CreateProjectTpaIntegrationWithBodyWithResponse request with arbitrary body returning *V1CreateProjectTpaIntegrationResponse -func (c *ClientWithResponses) V1CreateProjectTpaIntegrationWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CreateProjectTpaIntegrationResponse, error) { - rsp, err := c.V1CreateProjectTpaIntegrationWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateProjectTpaIntegrationResponse(rsp) -} - -func (c *ClientWithResponses) V1CreateProjectTpaIntegrationWithResponse(ctx context.Context, ref string, body V1CreateProjectTpaIntegrationJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CreateProjectTpaIntegrationResponse, error) { - rsp, err := c.V1CreateProjectTpaIntegration(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateProjectTpaIntegrationResponse(rsp) -} - -// V1DeleteProjectTpaIntegrationWithResponse request returning *V1DeleteProjectTpaIntegrationResponse -func (c *ClientWithResponses) V1DeleteProjectTpaIntegrationWithResponse(ctx context.Context, ref string, tpaId openapi_types.UUID, reqEditors ...RequestEditorFn) (*V1DeleteProjectTpaIntegrationResponse, error) { - rsp, err := c.V1DeleteProjectTpaIntegration(ctx, ref, tpaId, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1DeleteProjectTpaIntegrationResponse(rsp) -} - -// V1GetProjectTpaIntegrationWithResponse request returning *V1GetProjectTpaIntegrationResponse -func (c *ClientWithResponses) V1GetProjectTpaIntegrationWithResponse(ctx context.Context, ref string, tpaId openapi_types.UUID, reqEditors ...RequestEditorFn) (*V1GetProjectTpaIntegrationResponse, error) { - rsp, err := c.V1GetProjectTpaIntegration(ctx, ref, tpaId, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetProjectTpaIntegrationResponse(rsp) -} - -// V1GetProjectPgbouncerConfigWithResponse request returning *V1GetProjectPgbouncerConfigResponse -func (c *ClientWithResponses) V1GetProjectPgbouncerConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetProjectPgbouncerConfigResponse, error) { - rsp, err := c.V1GetProjectPgbouncerConfig(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetProjectPgbouncerConfigResponse(rsp) -} - -// V1GetPoolerConfigWithResponse request returning *V1GetPoolerConfigResponse -func (c *ClientWithResponses) V1GetPoolerConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetPoolerConfigResponse, error) { - rsp, err := c.V1GetPoolerConfig(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetPoolerConfigResponse(rsp) -} - -// V1UpdatePoolerConfigWithBodyWithResponse request with arbitrary body returning *V1UpdatePoolerConfigResponse -func (c *ClientWithResponses) V1UpdatePoolerConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdatePoolerConfigResponse, error) { - rsp, err := c.V1UpdatePoolerConfigWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdatePoolerConfigResponse(rsp) -} - -func (c *ClientWithResponses) V1UpdatePoolerConfigWithResponse(ctx context.Context, ref string, body V1UpdatePoolerConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdatePoolerConfigResponse, error) { - rsp, err := c.V1UpdatePoolerConfig(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdatePoolerConfigResponse(rsp) -} - -// V1GetPostgresConfigWithResponse request returning *V1GetPostgresConfigResponse -func (c *ClientWithResponses) V1GetPostgresConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetPostgresConfigResponse, error) { - rsp, err := c.V1GetPostgresConfig(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetPostgresConfigResponse(rsp) -} - -// V1UpdatePostgresConfigWithBodyWithResponse request with arbitrary body returning *V1UpdatePostgresConfigResponse -func (c *ClientWithResponses) V1UpdatePostgresConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdatePostgresConfigResponse, error) { - rsp, err := c.V1UpdatePostgresConfigWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdatePostgresConfigResponse(rsp) -} - -func (c *ClientWithResponses) V1UpdatePostgresConfigWithResponse(ctx context.Context, ref string, body V1UpdatePostgresConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdatePostgresConfigResponse, error) { - rsp, err := c.V1UpdatePostgresConfig(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdatePostgresConfigResponse(rsp) -} - -// V1GetDatabaseDiskWithResponse request returning *V1GetDatabaseDiskResponse -func (c *ClientWithResponses) V1GetDatabaseDiskWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetDatabaseDiskResponse, error) { - rsp, err := c.V1GetDatabaseDisk(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetDatabaseDiskResponse(rsp) -} - -// V1ModifyDatabaseDiskWithBodyWithResponse request with arbitrary body returning *V1ModifyDatabaseDiskResponse -func (c *ClientWithResponses) V1ModifyDatabaseDiskWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1ModifyDatabaseDiskResponse, error) { - rsp, err := c.V1ModifyDatabaseDiskWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ModifyDatabaseDiskResponse(rsp) -} - -func (c *ClientWithResponses) V1ModifyDatabaseDiskWithResponse(ctx context.Context, ref string, body V1ModifyDatabaseDiskJSONRequestBody, reqEditors ...RequestEditorFn) (*V1ModifyDatabaseDiskResponse, error) { - rsp, err := c.V1ModifyDatabaseDisk(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ModifyDatabaseDiskResponse(rsp) -} - -// V1GetProjectDiskAutoscaleConfigWithResponse request returning *V1GetProjectDiskAutoscaleConfigResponse -func (c *ClientWithResponses) V1GetProjectDiskAutoscaleConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetProjectDiskAutoscaleConfigResponse, error) { - rsp, err := c.V1GetProjectDiskAutoscaleConfig(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetProjectDiskAutoscaleConfigResponse(rsp) -} - -// V1GetDiskUtilizationWithResponse request returning *V1GetDiskUtilizationResponse -func (c *ClientWithResponses) V1GetDiskUtilizationWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetDiskUtilizationResponse, error) { - rsp, err := c.V1GetDiskUtilization(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetDiskUtilizationResponse(rsp) -} - -// V1GetRealtimeConfigWithResponse request returning *V1GetRealtimeConfigResponse -func (c *ClientWithResponses) V1GetRealtimeConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetRealtimeConfigResponse, error) { - rsp, err := c.V1GetRealtimeConfig(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetRealtimeConfigResponse(rsp) -} - -// V1UpdateRealtimeConfigWithBodyWithResponse request with arbitrary body returning *V1UpdateRealtimeConfigResponse -func (c *ClientWithResponses) V1UpdateRealtimeConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateRealtimeConfigResponse, error) { - rsp, err := c.V1UpdateRealtimeConfigWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateRealtimeConfigResponse(rsp) -} - -func (c *ClientWithResponses) V1UpdateRealtimeConfigWithResponse(ctx context.Context, ref string, body V1UpdateRealtimeConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateRealtimeConfigResponse, error) { - rsp, err := c.V1UpdateRealtimeConfig(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateRealtimeConfigResponse(rsp) -} - -// V1ShutdownRealtimeWithResponse request returning *V1ShutdownRealtimeResponse -func (c *ClientWithResponses) V1ShutdownRealtimeWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ShutdownRealtimeResponse, error) { - rsp, err := c.V1ShutdownRealtime(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ShutdownRealtimeResponse(rsp) -} - -// V1GetStorageConfigWithResponse request returning *V1GetStorageConfigResponse -func (c *ClientWithResponses) V1GetStorageConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetStorageConfigResponse, error) { - rsp, err := c.V1GetStorageConfig(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetStorageConfigResponse(rsp) -} - -// V1UpdateStorageConfigWithBodyWithResponse request with arbitrary body returning *V1UpdateStorageConfigResponse -func (c *ClientWithResponses) V1UpdateStorageConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateStorageConfigResponse, error) { - rsp, err := c.V1UpdateStorageConfigWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateStorageConfigResponse(rsp) -} - -func (c *ClientWithResponses) V1UpdateStorageConfigWithResponse(ctx context.Context, ref string, body V1UpdateStorageConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateStorageConfigResponse, error) { - rsp, err := c.V1UpdateStorageConfig(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateStorageConfigResponse(rsp) -} - -// V1DeleteHostnameConfigWithResponse request returning *V1DeleteHostnameConfigResponse -func (c *ClientWithResponses) V1DeleteHostnameConfigWithResponse(ctx context.Context, ref string, params *V1DeleteHostnameConfigParams, reqEditors ...RequestEditorFn) (*V1DeleteHostnameConfigResponse, error) { - rsp, err := c.V1DeleteHostnameConfig(ctx, ref, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1DeleteHostnameConfigResponse(rsp) -} - -// V1GetHostnameConfigWithResponse request returning *V1GetHostnameConfigResponse -func (c *ClientWithResponses) V1GetHostnameConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetHostnameConfigResponse, error) { - rsp, err := c.V1GetHostnameConfig(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetHostnameConfigResponse(rsp) -} - -// V1ActivateCustomHostnameWithResponse request returning *V1ActivateCustomHostnameResponse -func (c *ClientWithResponses) V1ActivateCustomHostnameWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ActivateCustomHostnameResponse, error) { - rsp, err := c.V1ActivateCustomHostname(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ActivateCustomHostnameResponse(rsp) -} - -// V1UpdateHostnameConfigWithBodyWithResponse request with arbitrary body returning *V1UpdateHostnameConfigResponse -func (c *ClientWithResponses) V1UpdateHostnameConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateHostnameConfigResponse, error) { - rsp, err := c.V1UpdateHostnameConfigWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateHostnameConfigResponse(rsp) -} - -func (c *ClientWithResponses) V1UpdateHostnameConfigWithResponse(ctx context.Context, ref string, body V1UpdateHostnameConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateHostnameConfigResponse, error) { - rsp, err := c.V1UpdateHostnameConfig(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateHostnameConfigResponse(rsp) -} - -// V1VerifyDnsConfigWithResponse request returning *V1VerifyDnsConfigResponse -func (c *ClientWithResponses) V1VerifyDnsConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1VerifyDnsConfigResponse, error) { - rsp, err := c.V1VerifyDnsConfig(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1VerifyDnsConfigResponse(rsp) -} - -// V1ListAllBackupsWithResponse request returning *V1ListAllBackupsResponse -func (c *ClientWithResponses) V1ListAllBackupsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListAllBackupsResponse, error) { - rsp, err := c.V1ListAllBackups(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ListAllBackupsResponse(rsp) -} - -// V1RestorePhysicalBackupWithBodyWithResponse request with arbitrary body returning *V1RestorePhysicalBackupResponse -func (c *ClientWithResponses) V1RestorePhysicalBackupWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1RestorePhysicalBackupResponse, error) { - rsp, err := c.V1RestorePhysicalBackupWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1RestorePhysicalBackupResponse(rsp) -} - -func (c *ClientWithResponses) V1RestorePhysicalBackupWithResponse(ctx context.Context, ref string, body V1RestorePhysicalBackupJSONRequestBody, reqEditors ...RequestEditorFn) (*V1RestorePhysicalBackupResponse, error) { - rsp, err := c.V1RestorePhysicalBackup(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1RestorePhysicalBackupResponse(rsp) -} - -// V1RestorePitrBackupWithBodyWithResponse request with arbitrary body returning *V1RestorePitrBackupResponse -func (c *ClientWithResponses) V1RestorePitrBackupWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1RestorePitrBackupResponse, error) { - rsp, err := c.V1RestorePitrBackupWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1RestorePitrBackupResponse(rsp) -} - -func (c *ClientWithResponses) V1RestorePitrBackupWithResponse(ctx context.Context, ref string, body V1RestorePitrBackupJSONRequestBody, reqEditors ...RequestEditorFn) (*V1RestorePitrBackupResponse, error) { - rsp, err := c.V1RestorePitrBackup(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1RestorePitrBackupResponse(rsp) -} - -// V1GetRestorePointWithResponse request returning *V1GetRestorePointResponse -func (c *ClientWithResponses) V1GetRestorePointWithResponse(ctx context.Context, ref string, params *V1GetRestorePointParams, reqEditors ...RequestEditorFn) (*V1GetRestorePointResponse, error) { - rsp, err := c.V1GetRestorePoint(ctx, ref, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetRestorePointResponse(rsp) -} - -// V1CreateRestorePointWithBodyWithResponse request with arbitrary body returning *V1CreateRestorePointResponse -func (c *ClientWithResponses) V1CreateRestorePointWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CreateRestorePointResponse, error) { - rsp, err := c.V1CreateRestorePointWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateRestorePointResponse(rsp) -} - -func (c *ClientWithResponses) V1CreateRestorePointWithResponse(ctx context.Context, ref string, body V1CreateRestorePointJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CreateRestorePointResponse, error) { - rsp, err := c.V1CreateRestorePoint(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateRestorePointResponse(rsp) -} - -// V1GetBackupScheduleWithResponse request returning *V1GetBackupScheduleResponse -func (c *ClientWithResponses) V1GetBackupScheduleWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetBackupScheduleResponse, error) { - rsp, err := c.V1GetBackupSchedule(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetBackupScheduleResponse(rsp) -} - -// V1UpdateBackupScheduleWithBodyWithResponse request with arbitrary body returning *V1UpdateBackupScheduleResponse -func (c *ClientWithResponses) V1UpdateBackupScheduleWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateBackupScheduleResponse, error) { - rsp, err := c.V1UpdateBackupScheduleWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateBackupScheduleResponse(rsp) -} - -func (c *ClientWithResponses) V1UpdateBackupScheduleWithResponse(ctx context.Context, ref string, body V1UpdateBackupScheduleJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateBackupScheduleResponse, error) { - rsp, err := c.V1UpdateBackupSchedule(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateBackupScheduleResponse(rsp) -} - -// V1UndoWithBodyWithResponse request with arbitrary body returning *V1UndoResponse -func (c *ClientWithResponses) V1UndoWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UndoResponse, error) { - rsp, err := c.V1UndoWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UndoResponse(rsp) -} - -func (c *ClientWithResponses) V1UndoWithResponse(ctx context.Context, ref string, body V1UndoJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UndoResponse, error) { - rsp, err := c.V1Undo(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UndoResponse(rsp) -} - -// V1GetDatabaseMetadataWithResponse request returning *V1GetDatabaseMetadataResponse -func (c *ClientWithResponses) V1GetDatabaseMetadataWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetDatabaseMetadataResponse, error) { - rsp, err := c.V1GetDatabaseMetadata(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetDatabaseMetadataResponse(rsp) -} - -// V1GetJitAccessWithResponse request returning *V1GetJitAccessResponse -func (c *ClientWithResponses) V1GetJitAccessWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetJitAccessResponse, error) { - rsp, err := c.V1GetJitAccess(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetJitAccessResponse(rsp) -} - -// V1AuthorizeJitAccessWithBodyWithResponse request with arbitrary body returning *V1AuthorizeJitAccessResponse -func (c *ClientWithResponses) V1AuthorizeJitAccessWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1AuthorizeJitAccessResponse, error) { - rsp, err := c.V1AuthorizeJitAccessWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1AuthorizeJitAccessResponse(rsp) -} - -func (c *ClientWithResponses) V1AuthorizeJitAccessWithResponse(ctx context.Context, ref string, body V1AuthorizeJitAccessJSONRequestBody, reqEditors ...RequestEditorFn) (*V1AuthorizeJitAccessResponse, error) { - rsp, err := c.V1AuthorizeJitAccess(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1AuthorizeJitAccessResponse(rsp) -} - -// V1UpdateJitAccessWithBodyWithResponse request with arbitrary body returning *V1UpdateJitAccessResponse -func (c *ClientWithResponses) V1UpdateJitAccessWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateJitAccessResponse, error) { - rsp, err := c.V1UpdateJitAccessWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateJitAccessResponse(rsp) -} - -func (c *ClientWithResponses) V1UpdateJitAccessWithResponse(ctx context.Context, ref string, body V1UpdateJitAccessJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateJitAccessResponse, error) { - rsp, err := c.V1UpdateJitAccess(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateJitAccessResponse(rsp) -} - -// V1InviteExternalJitAccessWithBodyWithResponse request with arbitrary body returning *V1InviteExternalJitAccessResponse -func (c *ClientWithResponses) V1InviteExternalJitAccessWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1InviteExternalJitAccessResponse, error) { - rsp, err := c.V1InviteExternalJitAccessWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1InviteExternalJitAccessResponse(rsp) -} - -func (c *ClientWithResponses) V1InviteExternalJitAccessWithResponse(ctx context.Context, ref string, body V1InviteExternalJitAccessJSONRequestBody, reqEditors ...RequestEditorFn) (*V1InviteExternalJitAccessResponse, error) { - rsp, err := c.V1InviteExternalJitAccess(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1InviteExternalJitAccessResponse(rsp) -} - -// V1AcceptInviteExternalJitAccessWithBodyWithResponse request with arbitrary body returning *V1AcceptInviteExternalJitAccessResponse -func (c *ClientWithResponses) V1AcceptInviteExternalJitAccessWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1AcceptInviteExternalJitAccessResponse, error) { - rsp, err := c.V1AcceptInviteExternalJitAccessWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1AcceptInviteExternalJitAccessResponse(rsp) -} - -func (c *ClientWithResponses) V1AcceptInviteExternalJitAccessWithResponse(ctx context.Context, ref string, body V1AcceptInviteExternalJitAccessJSONRequestBody, reqEditors ...RequestEditorFn) (*V1AcceptInviteExternalJitAccessResponse, error) { - rsp, err := c.V1AcceptInviteExternalJitAccess(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1AcceptInviteExternalJitAccessResponse(rsp) -} - -// V1DeleteInviteExternalJitAccessWithResponse request returning *V1DeleteInviteExternalJitAccessResponse -func (c *ClientWithResponses) V1DeleteInviteExternalJitAccessWithResponse(ctx context.Context, ref string, inviteId openapi_types.UUID, reqEditors ...RequestEditorFn) (*V1DeleteInviteExternalJitAccessResponse, error) { - rsp, err := c.V1DeleteInviteExternalJitAccess(ctx, ref, inviteId, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1DeleteInviteExternalJitAccessResponse(rsp) -} - -// V1ListJitAccessWithResponse request returning *V1ListJitAccessResponse -func (c *ClientWithResponses) V1ListJitAccessWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListJitAccessResponse, error) { - rsp, err := c.V1ListJitAccess(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ListJitAccessResponse(rsp) -} - -// V1DeleteJitAccessWithResponse request returning *V1DeleteJitAccessResponse -func (c *ClientWithResponses) V1DeleteJitAccessWithResponse(ctx context.Context, ref string, userId openapi_types.UUID, reqEditors ...RequestEditorFn) (*V1DeleteJitAccessResponse, error) { - rsp, err := c.V1DeleteJitAccess(ctx, ref, userId, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1DeleteJitAccessResponse(rsp) -} - -// V1RollbackMigrationsWithResponse request returning *V1RollbackMigrationsResponse -func (c *ClientWithResponses) V1RollbackMigrationsWithResponse(ctx context.Context, ref string, params *V1RollbackMigrationsParams, reqEditors ...RequestEditorFn) (*V1RollbackMigrationsResponse, error) { - rsp, err := c.V1RollbackMigrations(ctx, ref, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1RollbackMigrationsResponse(rsp) -} - -// V1ListMigrationHistoryWithResponse request returning *V1ListMigrationHistoryResponse -func (c *ClientWithResponses) V1ListMigrationHistoryWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListMigrationHistoryResponse, error) { - rsp, err := c.V1ListMigrationHistory(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ListMigrationHistoryResponse(rsp) -} - -// V1ApplyAMigrationWithBodyWithResponse request with arbitrary body returning *V1ApplyAMigrationResponse -func (c *ClientWithResponses) V1ApplyAMigrationWithBodyWithResponse(ctx context.Context, ref string, params *V1ApplyAMigrationParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1ApplyAMigrationResponse, error) { - rsp, err := c.V1ApplyAMigrationWithBody(ctx, ref, params, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ApplyAMigrationResponse(rsp) -} - -func (c *ClientWithResponses) V1ApplyAMigrationWithResponse(ctx context.Context, ref string, params *V1ApplyAMigrationParams, body V1ApplyAMigrationJSONRequestBody, reqEditors ...RequestEditorFn) (*V1ApplyAMigrationResponse, error) { - rsp, err := c.V1ApplyAMigration(ctx, ref, params, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ApplyAMigrationResponse(rsp) -} - -// V1UpsertAMigrationWithBodyWithResponse request with arbitrary body returning *V1UpsertAMigrationResponse -func (c *ClientWithResponses) V1UpsertAMigrationWithBodyWithResponse(ctx context.Context, ref string, params *V1UpsertAMigrationParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpsertAMigrationResponse, error) { - rsp, err := c.V1UpsertAMigrationWithBody(ctx, ref, params, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpsertAMigrationResponse(rsp) -} - -func (c *ClientWithResponses) V1UpsertAMigrationWithResponse(ctx context.Context, ref string, params *V1UpsertAMigrationParams, body V1UpsertAMigrationJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpsertAMigrationResponse, error) { - rsp, err := c.V1UpsertAMigration(ctx, ref, params, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpsertAMigrationResponse(rsp) -} - -// V1GetAMigrationWithResponse request returning *V1GetAMigrationResponse -func (c *ClientWithResponses) V1GetAMigrationWithResponse(ctx context.Context, ref string, version string, reqEditors ...RequestEditorFn) (*V1GetAMigrationResponse, error) { - rsp, err := c.V1GetAMigration(ctx, ref, version, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetAMigrationResponse(rsp) -} - -// V1PatchAMigrationWithBodyWithResponse request with arbitrary body returning *V1PatchAMigrationResponse -func (c *ClientWithResponses) V1PatchAMigrationWithBodyWithResponse(ctx context.Context, ref string, version string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1PatchAMigrationResponse, error) { - rsp, err := c.V1PatchAMigrationWithBody(ctx, ref, version, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1PatchAMigrationResponse(rsp) -} - -func (c *ClientWithResponses) V1PatchAMigrationWithResponse(ctx context.Context, ref string, version string, body V1PatchAMigrationJSONRequestBody, reqEditors ...RequestEditorFn) (*V1PatchAMigrationResponse, error) { - rsp, err := c.V1PatchAMigration(ctx, ref, version, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1PatchAMigrationResponse(rsp) -} - -// V1GetDatabaseOpenapiWithResponse request returning *V1GetDatabaseOpenapiResponse -func (c *ClientWithResponses) V1GetDatabaseOpenapiWithResponse(ctx context.Context, ref string, params *V1GetDatabaseOpenapiParams, reqEditors ...RequestEditorFn) (*V1GetDatabaseOpenapiResponse, error) { - rsp, err := c.V1GetDatabaseOpenapi(ctx, ref, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetDatabaseOpenapiResponse(rsp) -} - -// V1UpdateDatabasePasswordWithBodyWithResponse request with arbitrary body returning *V1UpdateDatabasePasswordResponse -func (c *ClientWithResponses) V1UpdateDatabasePasswordWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateDatabasePasswordResponse, error) { - rsp, err := c.V1UpdateDatabasePasswordWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateDatabasePasswordResponse(rsp) -} - -func (c *ClientWithResponses) V1UpdateDatabasePasswordWithResponse(ctx context.Context, ref string, body V1UpdateDatabasePasswordJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateDatabasePasswordResponse, error) { - rsp, err := c.V1UpdateDatabasePassword(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateDatabasePasswordResponse(rsp) -} - -// V1RunAQueryWithBodyWithResponse request with arbitrary body returning *V1RunAQueryResponse -func (c *ClientWithResponses) V1RunAQueryWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1RunAQueryResponse, error) { - rsp, err := c.V1RunAQueryWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1RunAQueryResponse(rsp) -} - -func (c *ClientWithResponses) V1RunAQueryWithResponse(ctx context.Context, ref string, body V1RunAQueryJSONRequestBody, reqEditors ...RequestEditorFn) (*V1RunAQueryResponse, error) { - rsp, err := c.V1RunAQuery(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1RunAQueryResponse(rsp) -} - -// V1ReadOnlyQueryWithBodyWithResponse request with arbitrary body returning *V1ReadOnlyQueryResponse -func (c *ClientWithResponses) V1ReadOnlyQueryWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1ReadOnlyQueryResponse, error) { - rsp, err := c.V1ReadOnlyQueryWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ReadOnlyQueryResponse(rsp) -} - -func (c *ClientWithResponses) V1ReadOnlyQueryWithResponse(ctx context.Context, ref string, body V1ReadOnlyQueryJSONRequestBody, reqEditors ...RequestEditorFn) (*V1ReadOnlyQueryResponse, error) { - rsp, err := c.V1ReadOnlyQuery(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ReadOnlyQueryResponse(rsp) -} - -// V1EnableDatabaseWebhookWithResponse request returning *V1EnableDatabaseWebhookResponse -func (c *ClientWithResponses) V1EnableDatabaseWebhookWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1EnableDatabaseWebhookResponse, error) { - rsp, err := c.V1EnableDatabaseWebhook(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1EnableDatabaseWebhookResponse(rsp) -} - -// V1ListAllFunctionsWithResponse request returning *V1ListAllFunctionsResponse -func (c *ClientWithResponses) V1ListAllFunctionsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListAllFunctionsResponse, error) { - rsp, err := c.V1ListAllFunctions(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ListAllFunctionsResponse(rsp) -} - -// V1CreateAFunctionWithBodyWithResponse request with arbitrary body returning *V1CreateAFunctionResponse -func (c *ClientWithResponses) V1CreateAFunctionWithBodyWithResponse(ctx context.Context, ref string, params *V1CreateAFunctionParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CreateAFunctionResponse, error) { - rsp, err := c.V1CreateAFunctionWithBody(ctx, ref, params, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateAFunctionResponse(rsp) -} - -func (c *ClientWithResponses) V1CreateAFunctionWithResponse(ctx context.Context, ref string, params *V1CreateAFunctionParams, body V1CreateAFunctionJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CreateAFunctionResponse, error) { - rsp, err := c.V1CreateAFunction(ctx, ref, params, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CreateAFunctionResponse(rsp) -} - -// V1BulkUpdateFunctionsWithBodyWithResponse request with arbitrary body returning *V1BulkUpdateFunctionsResponse -func (c *ClientWithResponses) V1BulkUpdateFunctionsWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1BulkUpdateFunctionsResponse, error) { - rsp, err := c.V1BulkUpdateFunctionsWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1BulkUpdateFunctionsResponse(rsp) -} - -func (c *ClientWithResponses) V1BulkUpdateFunctionsWithResponse(ctx context.Context, ref string, body V1BulkUpdateFunctionsJSONRequestBody, reqEditors ...RequestEditorFn) (*V1BulkUpdateFunctionsResponse, error) { - rsp, err := c.V1BulkUpdateFunctions(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1BulkUpdateFunctionsResponse(rsp) -} - -// V1DeployAFunctionWithBodyWithResponse request with arbitrary body returning *V1DeployAFunctionResponse -func (c *ClientWithResponses) V1DeployAFunctionWithBodyWithResponse(ctx context.Context, ref string, params *V1DeployAFunctionParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1DeployAFunctionResponse, error) { - rsp, err := c.V1DeployAFunctionWithBody(ctx, ref, params, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1DeployAFunctionResponse(rsp) -} - -// V1DeleteAFunctionWithResponse request returning *V1DeleteAFunctionResponse -func (c *ClientWithResponses) V1DeleteAFunctionWithResponse(ctx context.Context, ref string, functionSlug string, reqEditors ...RequestEditorFn) (*V1DeleteAFunctionResponse, error) { - rsp, err := c.V1DeleteAFunction(ctx, ref, functionSlug, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1DeleteAFunctionResponse(rsp) -} - -// V1GetAFunctionWithResponse request returning *V1GetAFunctionResponse -func (c *ClientWithResponses) V1GetAFunctionWithResponse(ctx context.Context, ref string, functionSlug string, reqEditors ...RequestEditorFn) (*V1GetAFunctionResponse, error) { - rsp, err := c.V1GetAFunction(ctx, ref, functionSlug, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetAFunctionResponse(rsp) -} - -// V1UpdateAFunctionWithBodyWithResponse request with arbitrary body returning *V1UpdateAFunctionResponse -func (c *ClientWithResponses) V1UpdateAFunctionWithBodyWithResponse(ctx context.Context, ref string, functionSlug string, params *V1UpdateAFunctionParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateAFunctionResponse, error) { - rsp, err := c.V1UpdateAFunctionWithBody(ctx, ref, functionSlug, params, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateAFunctionResponse(rsp) -} - -func (c *ClientWithResponses) V1UpdateAFunctionWithResponse(ctx context.Context, ref string, functionSlug string, params *V1UpdateAFunctionParams, body V1UpdateAFunctionJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateAFunctionResponse, error) { - rsp, err := c.V1UpdateAFunction(ctx, ref, functionSlug, params, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateAFunctionResponse(rsp) -} - -// V1GetAFunctionBodyWithResponse request returning *V1GetAFunctionBodyResponse -func (c *ClientWithResponses) V1GetAFunctionBodyWithResponse(ctx context.Context, ref string, functionSlug string, reqEditors ...RequestEditorFn) (*V1GetAFunctionBodyResponse, error) { - rsp, err := c.V1GetAFunctionBody(ctx, ref, functionSlug, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetAFunctionBodyResponse(rsp) -} - -// V1GetServicesHealthWithResponse request returning *V1GetServicesHealthResponse -func (c *ClientWithResponses) V1GetServicesHealthWithResponse(ctx context.Context, ref string, params *V1GetServicesHealthParams, reqEditors ...RequestEditorFn) (*V1GetServicesHealthResponse, error) { - rsp, err := c.V1GetServicesHealth(ctx, ref, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetServicesHealthResponse(rsp) -} - -// V1GetJitAccessConfigWithResponse request returning *V1GetJitAccessConfigResponse -func (c *ClientWithResponses) V1GetJitAccessConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetJitAccessConfigResponse, error) { - rsp, err := c.V1GetJitAccessConfig(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetJitAccessConfigResponse(rsp) -} - -// V1UpdateJitAccessConfigWithBodyWithResponse request with arbitrary body returning *V1UpdateJitAccessConfigResponse -func (c *ClientWithResponses) V1UpdateJitAccessConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateJitAccessConfigResponse, error) { - rsp, err := c.V1UpdateJitAccessConfigWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateJitAccessConfigResponse(rsp) -} - -func (c *ClientWithResponses) V1UpdateJitAccessConfigWithResponse(ctx context.Context, ref string, body V1UpdateJitAccessConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateJitAccessConfigResponse, error) { - rsp, err := c.V1UpdateJitAccessConfig(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateJitAccessConfigResponse(rsp) -} - -// V1DeleteNetworkBansWithBodyWithResponse request with arbitrary body returning *V1DeleteNetworkBansResponse -func (c *ClientWithResponses) V1DeleteNetworkBansWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1DeleteNetworkBansResponse, error) { - rsp, err := c.V1DeleteNetworkBansWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1DeleteNetworkBansResponse(rsp) -} - -func (c *ClientWithResponses) V1DeleteNetworkBansWithResponse(ctx context.Context, ref string, body V1DeleteNetworkBansJSONRequestBody, reqEditors ...RequestEditorFn) (*V1DeleteNetworkBansResponse, error) { - rsp, err := c.V1DeleteNetworkBans(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1DeleteNetworkBansResponse(rsp) -} - -// V1ListAllNetworkBansWithResponse request returning *V1ListAllNetworkBansResponse -func (c *ClientWithResponses) V1ListAllNetworkBansWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListAllNetworkBansResponse, error) { - rsp, err := c.V1ListAllNetworkBans(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ListAllNetworkBansResponse(rsp) -} - -// V1ListAllNetworkBansEnrichedWithResponse request returning *V1ListAllNetworkBansEnrichedResponse -func (c *ClientWithResponses) V1ListAllNetworkBansEnrichedWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListAllNetworkBansEnrichedResponse, error) { - rsp, err := c.V1ListAllNetworkBansEnriched(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ListAllNetworkBansEnrichedResponse(rsp) -} - -// V1GetNetworkRestrictionsWithResponse request returning *V1GetNetworkRestrictionsResponse -func (c *ClientWithResponses) V1GetNetworkRestrictionsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetNetworkRestrictionsResponse, error) { - rsp, err := c.V1GetNetworkRestrictions(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetNetworkRestrictionsResponse(rsp) -} - -// V1PatchNetworkRestrictionsWithBodyWithResponse request with arbitrary body returning *V1PatchNetworkRestrictionsResponse -func (c *ClientWithResponses) V1PatchNetworkRestrictionsWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1PatchNetworkRestrictionsResponse, error) { - rsp, err := c.V1PatchNetworkRestrictionsWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1PatchNetworkRestrictionsResponse(rsp) -} - -func (c *ClientWithResponses) V1PatchNetworkRestrictionsWithResponse(ctx context.Context, ref string, body V1PatchNetworkRestrictionsJSONRequestBody, reqEditors ...RequestEditorFn) (*V1PatchNetworkRestrictionsResponse, error) { - rsp, err := c.V1PatchNetworkRestrictions(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1PatchNetworkRestrictionsResponse(rsp) -} - -// V1UpdateNetworkRestrictionsWithBodyWithResponse request with arbitrary body returning *V1UpdateNetworkRestrictionsResponse -func (c *ClientWithResponses) V1UpdateNetworkRestrictionsWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateNetworkRestrictionsResponse, error) { - rsp, err := c.V1UpdateNetworkRestrictionsWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateNetworkRestrictionsResponse(rsp) -} - -func (c *ClientWithResponses) V1UpdateNetworkRestrictionsWithResponse(ctx context.Context, ref string, body V1UpdateNetworkRestrictionsJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateNetworkRestrictionsResponse, error) { - rsp, err := c.V1UpdateNetworkRestrictions(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateNetworkRestrictionsResponse(rsp) -} - -// V1PauseAProjectWithResponse request returning *V1PauseAProjectResponse -func (c *ClientWithResponses) V1PauseAProjectWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1PauseAProjectResponse, error) { - rsp, err := c.V1PauseAProject(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1PauseAProjectResponse(rsp) -} - -// V1GetPgsodiumConfigWithResponse request returning *V1GetPgsodiumConfigResponse -func (c *ClientWithResponses) V1GetPgsodiumConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetPgsodiumConfigResponse, error) { - rsp, err := c.V1GetPgsodiumConfig(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetPgsodiumConfigResponse(rsp) -} - -// V1UpdatePgsodiumConfigWithBodyWithResponse request with arbitrary body returning *V1UpdatePgsodiumConfigResponse -func (c *ClientWithResponses) V1UpdatePgsodiumConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdatePgsodiumConfigResponse, error) { - rsp, err := c.V1UpdatePgsodiumConfigWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdatePgsodiumConfigResponse(rsp) -} - -func (c *ClientWithResponses) V1UpdatePgsodiumConfigWithResponse(ctx context.Context, ref string, body V1UpdatePgsodiumConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdatePgsodiumConfigResponse, error) { - rsp, err := c.V1UpdatePgsodiumConfig(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdatePgsodiumConfigResponse(rsp) -} - -// V1GetPostgrestServiceConfigWithResponse request returning *V1GetPostgrestServiceConfigResponse -func (c *ClientWithResponses) V1GetPostgrestServiceConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetPostgrestServiceConfigResponse, error) { - rsp, err := c.V1GetPostgrestServiceConfig(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetPostgrestServiceConfigResponse(rsp) -} - -// V1UpdatePostgrestServiceConfigWithBodyWithResponse request with arbitrary body returning *V1UpdatePostgrestServiceConfigResponse -func (c *ClientWithResponses) V1UpdatePostgrestServiceConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdatePostgrestServiceConfigResponse, error) { - rsp, err := c.V1UpdatePostgrestServiceConfigWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdatePostgrestServiceConfigResponse(rsp) -} - -func (c *ClientWithResponses) V1UpdatePostgrestServiceConfigWithResponse(ctx context.Context, ref string, body V1UpdatePostgrestServiceConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdatePostgrestServiceConfigResponse, error) { - rsp, err := c.V1UpdatePostgrestServiceConfig(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdatePostgrestServiceConfigResponse(rsp) -} - -// V1RemoveAReadReplicaWithBodyWithResponse request with arbitrary body returning *V1RemoveAReadReplicaResponse -func (c *ClientWithResponses) V1RemoveAReadReplicaWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1RemoveAReadReplicaResponse, error) { - rsp, err := c.V1RemoveAReadReplicaWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1RemoveAReadReplicaResponse(rsp) -} - -func (c *ClientWithResponses) V1RemoveAReadReplicaWithResponse(ctx context.Context, ref string, body V1RemoveAReadReplicaJSONRequestBody, reqEditors ...RequestEditorFn) (*V1RemoveAReadReplicaResponse, error) { - rsp, err := c.V1RemoveAReadReplica(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1RemoveAReadReplicaResponse(rsp) -} - -// V1SetupAReadReplicaWithBodyWithResponse request with arbitrary body returning *V1SetupAReadReplicaResponse -func (c *ClientWithResponses) V1SetupAReadReplicaWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1SetupAReadReplicaResponse, error) { - rsp, err := c.V1SetupAReadReplicaWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1SetupAReadReplicaResponse(rsp) -} - -func (c *ClientWithResponses) V1SetupAReadReplicaWithResponse(ctx context.Context, ref string, body V1SetupAReadReplicaJSONRequestBody, reqEditors ...RequestEditorFn) (*V1SetupAReadReplicaResponse, error) { - rsp, err := c.V1SetupAReadReplica(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1SetupAReadReplicaResponse(rsp) -} - -// V1GetReadonlyModeStatusWithResponse request returning *V1GetReadonlyModeStatusResponse -func (c *ClientWithResponses) V1GetReadonlyModeStatusWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetReadonlyModeStatusResponse, error) { - rsp, err := c.V1GetReadonlyModeStatus(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetReadonlyModeStatusResponse(rsp) -} - -// V1DisableReadonlyModeTemporarilyWithResponse request returning *V1DisableReadonlyModeTemporarilyResponse -func (c *ClientWithResponses) V1DisableReadonlyModeTemporarilyWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1DisableReadonlyModeTemporarilyResponse, error) { - rsp, err := c.V1DisableReadonlyModeTemporarily(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1DisableReadonlyModeTemporarilyResponse(rsp) -} - -// V1RestartAProjectWithResponse request returning *V1RestartAProjectResponse -func (c *ClientWithResponses) V1RestartAProjectWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1RestartAProjectResponse, error) { - rsp, err := c.V1RestartAProject(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1RestartAProjectResponse(rsp) -} - -// V1ListAvailableRestoreVersionsWithResponse request returning *V1ListAvailableRestoreVersionsResponse -func (c *ClientWithResponses) V1ListAvailableRestoreVersionsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListAvailableRestoreVersionsResponse, error) { - rsp, err := c.V1ListAvailableRestoreVersions(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ListAvailableRestoreVersionsResponse(rsp) -} - -// V1RestoreAProjectWithResponse request returning *V1RestoreAProjectResponse -func (c *ClientWithResponses) V1RestoreAProjectWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1RestoreAProjectResponse, error) { - rsp, err := c.V1RestoreAProject(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1RestoreAProjectResponse(rsp) -} - -// V1CancelAProjectRestorationWithResponse request returning *V1CancelAProjectRestorationResponse -func (c *ClientWithResponses) V1CancelAProjectRestorationWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1CancelAProjectRestorationResponse, error) { - rsp, err := c.V1CancelAProjectRestoration(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CancelAProjectRestorationResponse(rsp) -} - -// V1BulkDeleteSecretsWithBodyWithResponse request with arbitrary body returning *V1BulkDeleteSecretsResponse -func (c *ClientWithResponses) V1BulkDeleteSecretsWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1BulkDeleteSecretsResponse, error) { - rsp, err := c.V1BulkDeleteSecretsWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1BulkDeleteSecretsResponse(rsp) -} - -func (c *ClientWithResponses) V1BulkDeleteSecretsWithResponse(ctx context.Context, ref string, body V1BulkDeleteSecretsJSONRequestBody, reqEditors ...RequestEditorFn) (*V1BulkDeleteSecretsResponse, error) { - rsp, err := c.V1BulkDeleteSecrets(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1BulkDeleteSecretsResponse(rsp) -} - -// V1ListAllSecretsWithResponse request returning *V1ListAllSecretsResponse -func (c *ClientWithResponses) V1ListAllSecretsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListAllSecretsResponse, error) { - rsp, err := c.V1ListAllSecrets(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ListAllSecretsResponse(rsp) -} - -// V1BulkCreateSecretsWithBodyWithResponse request with arbitrary body returning *V1BulkCreateSecretsResponse -func (c *ClientWithResponses) V1BulkCreateSecretsWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1BulkCreateSecretsResponse, error) { - rsp, err := c.V1BulkCreateSecretsWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1BulkCreateSecretsResponse(rsp) -} - -func (c *ClientWithResponses) V1BulkCreateSecretsWithResponse(ctx context.Context, ref string, body V1BulkCreateSecretsJSONRequestBody, reqEditors ...RequestEditorFn) (*V1BulkCreateSecretsResponse, error) { - rsp, err := c.V1BulkCreateSecrets(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1BulkCreateSecretsResponse(rsp) -} - -// V1GetSslEnforcementConfigWithResponse request returning *V1GetSslEnforcementConfigResponse -func (c *ClientWithResponses) V1GetSslEnforcementConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetSslEnforcementConfigResponse, error) { - rsp, err := c.V1GetSslEnforcementConfig(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetSslEnforcementConfigResponse(rsp) -} - -// V1UpdateSslEnforcementConfigWithBodyWithResponse request with arbitrary body returning *V1UpdateSslEnforcementConfigResponse -func (c *ClientWithResponses) V1UpdateSslEnforcementConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateSslEnforcementConfigResponse, error) { - rsp, err := c.V1UpdateSslEnforcementConfigWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateSslEnforcementConfigResponse(rsp) -} - -func (c *ClientWithResponses) V1UpdateSslEnforcementConfigWithResponse(ctx context.Context, ref string, body V1UpdateSslEnforcementConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateSslEnforcementConfigResponse, error) { - rsp, err := c.V1UpdateSslEnforcementConfig(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpdateSslEnforcementConfigResponse(rsp) -} - -// V1ListAllBucketsWithResponse request returning *V1ListAllBucketsResponse -func (c *ClientWithResponses) V1ListAllBucketsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ListAllBucketsResponse, error) { - rsp, err := c.V1ListAllBuckets(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ListAllBucketsResponse(rsp) -} - -// V1GenerateTypescriptTypesWithResponse request returning *V1GenerateTypescriptTypesResponse -func (c *ClientWithResponses) V1GenerateTypescriptTypesWithResponse(ctx context.Context, ref string, params *V1GenerateTypescriptTypesParams, reqEditors ...RequestEditorFn) (*V1GenerateTypescriptTypesResponse, error) { - rsp, err := c.V1GenerateTypescriptTypes(ctx, ref, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GenerateTypescriptTypesResponse(rsp) -} - -// V1UpgradePostgresVersionWithBodyWithResponse request with arbitrary body returning *V1UpgradePostgresVersionResponse -func (c *ClientWithResponses) V1UpgradePostgresVersionWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpgradePostgresVersionResponse, error) { - rsp, err := c.V1UpgradePostgresVersionWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpgradePostgresVersionResponse(rsp) -} - -func (c *ClientWithResponses) V1UpgradePostgresVersionWithResponse(ctx context.Context, ref string, body V1UpgradePostgresVersionJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpgradePostgresVersionResponse, error) { - rsp, err := c.V1UpgradePostgresVersion(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1UpgradePostgresVersionResponse(rsp) -} - -// V1GetPostgresUpgradeEligibilityWithResponse request returning *V1GetPostgresUpgradeEligibilityResponse -func (c *ClientWithResponses) V1GetPostgresUpgradeEligibilityWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetPostgresUpgradeEligibilityResponse, error) { - rsp, err := c.V1GetPostgresUpgradeEligibility(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetPostgresUpgradeEligibilityResponse(rsp) -} - -// V1GetPostgresUpgradeStatusWithResponse request returning *V1GetPostgresUpgradeStatusResponse -func (c *ClientWithResponses) V1GetPostgresUpgradeStatusWithResponse(ctx context.Context, ref string, params *V1GetPostgresUpgradeStatusParams, reqEditors ...RequestEditorFn) (*V1GetPostgresUpgradeStatusResponse, error) { - rsp, err := c.V1GetPostgresUpgradeStatus(ctx, ref, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetPostgresUpgradeStatusResponse(rsp) -} - -// V1DeactivateVanitySubdomainConfigWithResponse request returning *V1DeactivateVanitySubdomainConfigResponse -func (c *ClientWithResponses) V1DeactivateVanitySubdomainConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1DeactivateVanitySubdomainConfigResponse, error) { - rsp, err := c.V1DeactivateVanitySubdomainConfig(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1DeactivateVanitySubdomainConfigResponse(rsp) -} - -// V1GetVanitySubdomainConfigWithResponse request returning *V1GetVanitySubdomainConfigResponse -func (c *ClientWithResponses) V1GetVanitySubdomainConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetVanitySubdomainConfigResponse, error) { - rsp, err := c.V1GetVanitySubdomainConfig(ctx, ref, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetVanitySubdomainConfigResponse(rsp) -} - -// V1ActivateVanitySubdomainConfigWithBodyWithResponse request with arbitrary body returning *V1ActivateVanitySubdomainConfigResponse -func (c *ClientWithResponses) V1ActivateVanitySubdomainConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1ActivateVanitySubdomainConfigResponse, error) { - rsp, err := c.V1ActivateVanitySubdomainConfigWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ActivateVanitySubdomainConfigResponse(rsp) -} - -func (c *ClientWithResponses) V1ActivateVanitySubdomainConfigWithResponse(ctx context.Context, ref string, body V1ActivateVanitySubdomainConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1ActivateVanitySubdomainConfigResponse, error) { - rsp, err := c.V1ActivateVanitySubdomainConfig(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ActivateVanitySubdomainConfigResponse(rsp) -} - -// V1CheckVanitySubdomainAvailabilityWithBodyWithResponse request with arbitrary body returning *V1CheckVanitySubdomainAvailabilityResponse -func (c *ClientWithResponses) V1CheckVanitySubdomainAvailabilityWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1CheckVanitySubdomainAvailabilityResponse, error) { - rsp, err := c.V1CheckVanitySubdomainAvailabilityWithBody(ctx, ref, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CheckVanitySubdomainAvailabilityResponse(rsp) -} - -func (c *ClientWithResponses) V1CheckVanitySubdomainAvailabilityWithResponse(ctx context.Context, ref string, body V1CheckVanitySubdomainAvailabilityJSONRequestBody, reqEditors ...RequestEditorFn) (*V1CheckVanitySubdomainAvailabilityResponse, error) { - rsp, err := c.V1CheckVanitySubdomainAvailability(ctx, ref, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1CheckVanitySubdomainAvailabilityResponse(rsp) -} - -// V1ListAllSnippetsWithResponse request returning *V1ListAllSnippetsResponse -func (c *ClientWithResponses) V1ListAllSnippetsWithResponse(ctx context.Context, params *V1ListAllSnippetsParams, reqEditors ...RequestEditorFn) (*V1ListAllSnippetsResponse, error) { - rsp, err := c.V1ListAllSnippets(ctx, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1ListAllSnippetsResponse(rsp) -} - -// V1GetASnippetWithResponse request returning *V1GetASnippetResponse -func (c *ClientWithResponses) V1GetASnippetWithResponse(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*V1GetASnippetResponse, error) { - rsp, err := c.V1GetASnippet(ctx, id, reqEditors...) - if err != nil { - return nil, err - } - return ParseV1GetASnippetResponse(rsp) -} - -// ParseV1DeleteABranchResponse parses an HTTP response from a V1DeleteABranchWithResponse call -func ParseV1DeleteABranchResponse(rsp *http.Response) (*V1DeleteABranchResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1DeleteABranchResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest BranchDeleteResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetABranchConfigResponse parses an HTTP response from a V1GetABranchConfigWithResponse call -func ParseV1GetABranchConfigResponse(rsp *http.Response) (*V1GetABranchConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetABranchConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest BranchDetailResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1UpdateABranchConfigResponse parses an HTTP response from a V1UpdateABranchConfigWithResponse call -func ParseV1UpdateABranchConfigResponse(rsp *http.Response) (*V1UpdateABranchConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdateABranchConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest BranchResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1DiffABranchResponse parses an HTTP response from a V1DiffABranchWithResponse call -func ParseV1DiffABranchResponse(rsp *http.Response) (*V1DiffABranchResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1DiffABranchResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1MergeABranchResponse parses an HTTP response from a V1MergeABranchWithResponse call -func ParseV1MergeABranchResponse(rsp *http.Response) (*V1MergeABranchResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1MergeABranchResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest BranchUpdateResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1PushABranchResponse parses an HTTP response from a V1PushABranchWithResponse call -func ParseV1PushABranchResponse(rsp *http.Response) (*V1PushABranchResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1PushABranchResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest BranchUpdateResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1ResetABranchResponse parses an HTTP response from a V1ResetABranchWithResponse call -func ParseV1ResetABranchResponse(rsp *http.Response) (*V1ResetABranchResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ResetABranchResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest BranchUpdateResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1RestoreABranchResponse parses an HTTP response from a V1RestoreABranchWithResponse call -func ParseV1RestoreABranchResponse(rsp *http.Response) (*V1RestoreABranchResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1RestoreABranchResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest BranchRestoreResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1AuthorizeUserResponse parses an HTTP response from a V1AuthorizeUserWithResponse call -func ParseV1AuthorizeUserResponse(rsp *http.Response) (*V1AuthorizeUserResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1AuthorizeUserResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1OauthAuthorizeProjectClaimResponse parses an HTTP response from a V1OauthAuthorizeProjectClaimWithResponse call -func ParseV1OauthAuthorizeProjectClaimResponse(rsp *http.Response) (*V1OauthAuthorizeProjectClaimResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1OauthAuthorizeProjectClaimResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1RevokeTokenResponse parses an HTTP response from a V1RevokeTokenWithResponse call -func ParseV1RevokeTokenResponse(rsp *http.Response) (*V1RevokeTokenResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1RevokeTokenResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1ExchangeOauthTokenResponse parses an HTTP response from a V1ExchangeOauthTokenWithResponse call -func ParseV1ExchangeOauthTokenResponse(rsp *http.Response) (*V1ExchangeOauthTokenResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ExchangeOauthTokenResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest OAuthTokenResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1ListAllOrganizationsResponse parses an HTTP response from a V1ListAllOrganizationsWithResponse call -func ParseV1ListAllOrganizationsResponse(rsp *http.Response) (*V1ListAllOrganizationsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ListAllOrganizationsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest []OrganizationResponseV1Output - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1CreateAnOrganizationResponse parses an HTTP response from a V1CreateAnOrganizationWithResponse call -func ParseV1CreateAnOrganizationResponse(rsp *http.Response) (*V1CreateAnOrganizationResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1CreateAnOrganizationResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest OrganizationResponseV1Output - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1GetAnOrganizationResponse parses an HTTP response from a V1GetAnOrganizationWithResponse call -func ParseV1GetAnOrganizationResponse(rsp *http.Response) (*V1GetAnOrganizationResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetAnOrganizationResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest V1OrganizationSlugResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetOrganizationEntitlementsResponse parses an HTTP response from a V1GetOrganizationEntitlementsWithResponse call -func ParseV1GetOrganizationEntitlementsResponse(rsp *http.Response) (*V1GetOrganizationEntitlementsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetOrganizationEntitlementsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest V1ListEntitlementsResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1ListOrganizationMembersResponse parses an HTTP response from a V1ListOrganizationMembersWithResponse call -func ParseV1ListOrganizationMembersResponse(rsp *http.Response) (*V1ListOrganizationMembersResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ListOrganizationMembersResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest []V1OrganizationMemberResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetOrganizationProjectClaimResponse parses an HTTP response from a V1GetOrganizationProjectClaimWithResponse call -func ParseV1GetOrganizationProjectClaimResponse(rsp *http.Response) (*V1GetOrganizationProjectClaimResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetOrganizationProjectClaimResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest OrganizationProjectClaimResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1ClaimProjectForOrganizationResponse parses an HTTP response from a V1ClaimProjectForOrganizationWithResponse call -func ParseV1ClaimProjectForOrganizationResponse(rsp *http.Response) (*V1ClaimProjectForOrganizationResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ClaimProjectForOrganizationResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1GetAllProjectsForOrganizationResponse parses an HTTP response from a V1GetAllProjectsForOrganizationWithResponse call -func ParseV1GetAllProjectsForOrganizationResponse(rsp *http.Response) (*V1GetAllProjectsForOrganizationResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetAllProjectsForOrganizationResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest OrganizationProjectsResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetProfileResponse parses an HTTP response from a V1GetProfileWithResponse call -func ParseV1GetProfileResponse(rsp *http.Response) (*V1GetProfileResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetProfileResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest V1ProfileResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1ListAllProjectsResponse parses an HTTP response from a V1ListAllProjectsWithResponse call -func ParseV1ListAllProjectsResponse(rsp *http.Response) (*V1ListAllProjectsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ListAllProjectsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest []V1ProjectWithDatabaseResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1CreateAProjectResponse parses an HTTP response from a V1CreateAProjectWithResponse call -func ParseV1CreateAProjectResponse(rsp *http.Response) (*V1CreateAProjectResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1CreateAProjectResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest V1ProjectResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1GetAvailableRegionsResponse parses an HTTP response from a V1GetAvailableRegionsWithResponse call -func ParseV1GetAvailableRegionsResponse(rsp *http.Response) (*V1GetAvailableRegionsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetAvailableRegionsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest RegionsInfoV1Output - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1DeleteAProjectResponse parses an HTTP response from a V1DeleteAProjectWithResponse call -func ParseV1DeleteAProjectResponse(rsp *http.Response) (*V1DeleteAProjectResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1DeleteAProjectResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest V1ProjectRefResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetProjectResponse parses an HTTP response from a V1GetProjectWithResponse call -func ParseV1GetProjectResponse(rsp *http.Response) (*V1GetProjectResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetProjectResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest V1ProjectWithDatabaseResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1UpdateAProjectResponse parses an HTTP response from a V1UpdateAProjectWithResponse call -func ParseV1UpdateAProjectResponse(rsp *http.Response) (*V1UpdateAProjectResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdateAProjectResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest V1ProjectRefResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1ListActionRunsResponse parses an HTTP response from a V1ListActionRunsWithResponse call -func ParseV1ListActionRunsResponse(rsp *http.Response) (*V1ListActionRunsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ListActionRunsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest ListActionRunResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1CountActionRunsResponse parses an HTTP response from a V1CountActionRunsWithResponse call -func ParseV1CountActionRunsResponse(rsp *http.Response) (*V1CountActionRunsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1CountActionRunsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1GetActionRunResponse parses an HTTP response from a V1GetActionRunWithResponse call -func ParseV1GetActionRunResponse(rsp *http.Response) (*V1GetActionRunResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetActionRunResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest ActionRunResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetActionRunLogsResponse parses an HTTP response from a V1GetActionRunLogsWithResponse call -func ParseV1GetActionRunLogsResponse(rsp *http.Response) (*V1GetActionRunLogsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetActionRunLogsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1UpdateActionRunStatusResponse parses an HTTP response from a V1UpdateActionRunStatusWithResponse call -func ParseV1UpdateActionRunStatusResponse(rsp *http.Response) (*V1UpdateActionRunStatusResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdateActionRunStatusResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest UpdateRunStatusResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetPerformanceAdvisorsResponse parses an HTTP response from a V1GetPerformanceAdvisorsWithResponse call -func ParseV1GetPerformanceAdvisorsResponse(rsp *http.Response) (*V1GetPerformanceAdvisorsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetPerformanceAdvisorsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest V1ProjectAdvisorsResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetSecurityAdvisorsResponse parses an HTTP response from a V1GetSecurityAdvisorsWithResponse call -func ParseV1GetSecurityAdvisorsResponse(rsp *http.Response) (*V1GetSecurityAdvisorsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetSecurityAdvisorsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest V1ProjectAdvisorsResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetProjectFunctionCombinedStatsResponse parses an HTTP response from a V1GetProjectFunctionCombinedStatsWithResponse call -func ParseV1GetProjectFunctionCombinedStatsResponse(rsp *http.Response) (*V1GetProjectFunctionCombinedStatsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetProjectFunctionCombinedStatsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest AnalyticsResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetProjectLogsResponse parses an HTTP response from a V1GetProjectLogsWithResponse call -func ParseV1GetProjectLogsResponse(rsp *http.Response) (*V1GetProjectLogsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetProjectLogsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest AnalyticsResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetProjectLogsAllResponse parses an HTTP response from a V1GetProjectLogsAllWithResponse call -func ParseV1GetProjectLogsAllResponse(rsp *http.Response) (*V1GetProjectLogsAllResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetProjectLogsAllResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1ScrapeProjectMetricsResponse parses an HTTP response from a V1ScrapeProjectMetricsWithResponse call -func ParseV1ScrapeProjectMetricsResponse(rsp *http.Response) (*V1ScrapeProjectMetricsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ScrapeProjectMetricsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1GetProjectUsageApiCountResponse parses an HTTP response from a V1GetProjectUsageApiCountWithResponse call -func ParseV1GetProjectUsageApiCountResponse(rsp *http.Response) (*V1GetProjectUsageApiCountResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetProjectUsageApiCountResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest V1GetUsageApiCountResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetProjectUsageRequestCountResponse parses an HTTP response from a V1GetProjectUsageRequestCountWithResponse call -func ParseV1GetProjectUsageRequestCountResponse(rsp *http.Response) (*V1GetProjectUsageRequestCountResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetProjectUsageRequestCountResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest V1GetUsageApiRequestsCountResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetProjectApiKeysResponse parses an HTTP response from a V1GetProjectApiKeysWithResponse call -func ParseV1GetProjectApiKeysResponse(rsp *http.Response) (*V1GetProjectApiKeysResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetProjectApiKeysResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest []ApiKeyResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1CreateProjectApiKeyResponse parses an HTTP response from a V1CreateProjectApiKeyWithResponse call -func ParseV1CreateProjectApiKeyResponse(rsp *http.Response) (*V1CreateProjectApiKeyResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1CreateProjectApiKeyResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest ApiKeyResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1GetProjectLegacyApiKeysResponse parses an HTTP response from a V1GetProjectLegacyApiKeysWithResponse call -func ParseV1GetProjectLegacyApiKeysResponse(rsp *http.Response) (*V1GetProjectLegacyApiKeysResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetProjectLegacyApiKeysResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest LegacyApiKeysResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1UpdateProjectLegacyApiKeysResponse parses an HTTP response from a V1UpdateProjectLegacyApiKeysWithResponse call -func ParseV1UpdateProjectLegacyApiKeysResponse(rsp *http.Response) (*V1UpdateProjectLegacyApiKeysResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdateProjectLegacyApiKeysResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest LegacyApiKeysResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1DeleteProjectApiKeyResponse parses an HTTP response from a V1DeleteProjectApiKeyWithResponse call -func ParseV1DeleteProjectApiKeyResponse(rsp *http.Response) (*V1DeleteProjectApiKeyResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1DeleteProjectApiKeyResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest ApiKeyResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetProjectApiKeyResponse parses an HTTP response from a V1GetProjectApiKeyWithResponse call -func ParseV1GetProjectApiKeyResponse(rsp *http.Response) (*V1GetProjectApiKeyResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetProjectApiKeyResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest ApiKeyResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1UpdateProjectApiKeyResponse parses an HTTP response from a V1UpdateProjectApiKeyWithResponse call -func ParseV1UpdateProjectApiKeyResponse(rsp *http.Response) (*V1UpdateProjectApiKeyResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdateProjectApiKeyResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest ApiKeyResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1ListProjectAddonsResponse parses an HTTP response from a V1ListProjectAddonsWithResponse call -func ParseV1ListProjectAddonsResponse(rsp *http.Response) (*V1ListProjectAddonsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ListProjectAddonsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest ListProjectAddonsResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1ApplyProjectAddonResponse parses an HTTP response from a V1ApplyProjectAddonWithResponse call -func ParseV1ApplyProjectAddonResponse(rsp *http.Response) (*V1ApplyProjectAddonResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ApplyProjectAddonResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1RemoveProjectAddonResponse parses an HTTP response from a V1RemoveProjectAddonWithResponse call -func ParseV1RemoveProjectAddonResponse(rsp *http.Response) (*V1RemoveProjectAddonResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1RemoveProjectAddonResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1DisablePreviewBranchingResponse parses an HTTP response from a V1DisablePreviewBranchingWithResponse call -func ParseV1DisablePreviewBranchingResponse(rsp *http.Response) (*V1DisablePreviewBranchingResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1DisablePreviewBranchingResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1ListAllBranchesResponse parses an HTTP response from a V1ListAllBranchesWithResponse call -func ParseV1ListAllBranchesResponse(rsp *http.Response) (*V1ListAllBranchesResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ListAllBranchesResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest []BranchResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1CreateABranchResponse parses an HTTP response from a V1CreateABranchWithResponse call -func ParseV1CreateABranchResponse(rsp *http.Response) (*V1CreateABranchResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1CreateABranchResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest BranchResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1GetABranchResponse parses an HTTP response from a V1GetABranchWithResponse call -func ParseV1GetABranchResponse(rsp *http.Response) (*V1GetABranchResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetABranchResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest BranchResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1DeleteProjectClaimTokenResponse parses an HTTP response from a V1DeleteProjectClaimTokenWithResponse call -func ParseV1DeleteProjectClaimTokenResponse(rsp *http.Response) (*V1DeleteProjectClaimTokenResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1DeleteProjectClaimTokenResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1GetProjectClaimTokenResponse parses an HTTP response from a V1GetProjectClaimTokenWithResponse call -func ParseV1GetProjectClaimTokenResponse(rsp *http.Response) (*V1GetProjectClaimTokenResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetProjectClaimTokenResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest ProjectClaimTokenResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1CreateProjectClaimTokenResponse parses an HTTP response from a V1CreateProjectClaimTokenWithResponse call -func ParseV1CreateProjectClaimTokenResponse(rsp *http.Response) (*V1CreateProjectClaimTokenResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1CreateProjectClaimTokenResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest CreateProjectClaimTokenResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1DeleteLoginRolesResponse parses an HTTP response from a V1DeleteLoginRolesWithResponse call -func ParseV1DeleteLoginRolesResponse(rsp *http.Response) (*V1DeleteLoginRolesResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1DeleteLoginRolesResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest DeleteRolesResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1CreateLoginRoleResponse parses an HTTP response from a V1CreateLoginRoleWithResponse call -func ParseV1CreateLoginRoleResponse(rsp *http.Response) (*V1CreateLoginRoleResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1CreateLoginRoleResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest CreateRoleResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1GetAuthServiceConfigResponse parses an HTTP response from a V1GetAuthServiceConfigWithResponse call -func ParseV1GetAuthServiceConfigResponse(rsp *http.Response) (*V1GetAuthServiceConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetAuthServiceConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest AuthConfigResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1UpdateAuthServiceConfigResponse parses an HTTP response from a V1UpdateAuthServiceConfigWithResponse call -func ParseV1UpdateAuthServiceConfigResponse(rsp *http.Response) (*V1UpdateAuthServiceConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdateAuthServiceConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest AuthConfigResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetProjectSigningKeysResponse parses an HTTP response from a V1GetProjectSigningKeysWithResponse call -func ParseV1GetProjectSigningKeysResponse(rsp *http.Response) (*V1GetProjectSigningKeysResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetProjectSigningKeysResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest SigningKeysResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1CreateProjectSigningKeyResponse parses an HTTP response from a V1CreateProjectSigningKeyWithResponse call -func ParseV1CreateProjectSigningKeyResponse(rsp *http.Response) (*V1CreateProjectSigningKeyResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1CreateProjectSigningKeyResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest SigningKeyResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1GetLegacySigningKeyResponse parses an HTTP response from a V1GetLegacySigningKeyWithResponse call -func ParseV1GetLegacySigningKeyResponse(rsp *http.Response) (*V1GetLegacySigningKeyResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetLegacySigningKeyResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest SigningKeyResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1CreateLegacySigningKeyResponse parses an HTTP response from a V1CreateLegacySigningKeyWithResponse call -func ParseV1CreateLegacySigningKeyResponse(rsp *http.Response) (*V1CreateLegacySigningKeyResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1CreateLegacySigningKeyResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest SigningKeyResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1RemoveProjectSigningKeyResponse parses an HTTP response from a V1RemoveProjectSigningKeyWithResponse call -func ParseV1RemoveProjectSigningKeyResponse(rsp *http.Response) (*V1RemoveProjectSigningKeyResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1RemoveProjectSigningKeyResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest SigningKeyResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetProjectSigningKeyResponse parses an HTTP response from a V1GetProjectSigningKeyWithResponse call -func ParseV1GetProjectSigningKeyResponse(rsp *http.Response) (*V1GetProjectSigningKeyResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetProjectSigningKeyResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest SigningKeyResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1UpdateProjectSigningKeyResponse parses an HTTP response from a V1UpdateProjectSigningKeyWithResponse call -func ParseV1UpdateProjectSigningKeyResponse(rsp *http.Response) (*V1UpdateProjectSigningKeyResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdateProjectSigningKeyResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest SigningKeyResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1ListAllSsoProviderResponse parses an HTTP response from a V1ListAllSsoProviderWithResponse call -func ParseV1ListAllSsoProviderResponse(rsp *http.Response) (*V1ListAllSsoProviderResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ListAllSsoProviderResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest ListProvidersResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1CreateASsoProviderResponse parses an HTTP response from a V1CreateASsoProviderWithResponse call -func ParseV1CreateASsoProviderResponse(rsp *http.Response) (*V1CreateASsoProviderResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1CreateASsoProviderResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest CreateProviderResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1DeleteASsoProviderResponse parses an HTTP response from a V1DeleteASsoProviderWithResponse call -func ParseV1DeleteASsoProviderResponse(rsp *http.Response) (*V1DeleteASsoProviderResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1DeleteASsoProviderResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest DeleteProviderResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetASsoProviderResponse parses an HTTP response from a V1GetASsoProviderWithResponse call -func ParseV1GetASsoProviderResponse(rsp *http.Response) (*V1GetASsoProviderResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetASsoProviderResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest GetProviderResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1UpdateASsoProviderResponse parses an HTTP response from a V1UpdateASsoProviderWithResponse call -func ParseV1UpdateASsoProviderResponse(rsp *http.Response) (*V1UpdateASsoProviderResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdateASsoProviderResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest UpdateProviderResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1ListProjectTpaIntegrationsResponse parses an HTTP response from a V1ListProjectTpaIntegrationsWithResponse call -func ParseV1ListProjectTpaIntegrationsResponse(rsp *http.Response) (*V1ListProjectTpaIntegrationsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ListProjectTpaIntegrationsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest []ThirdPartyAuthOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1CreateProjectTpaIntegrationResponse parses an HTTP response from a V1CreateProjectTpaIntegrationWithResponse call -func ParseV1CreateProjectTpaIntegrationResponse(rsp *http.Response) (*V1CreateProjectTpaIntegrationResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1CreateProjectTpaIntegrationResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest ThirdPartyAuthOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1DeleteProjectTpaIntegrationResponse parses an HTTP response from a V1DeleteProjectTpaIntegrationWithResponse call -func ParseV1DeleteProjectTpaIntegrationResponse(rsp *http.Response) (*V1DeleteProjectTpaIntegrationResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1DeleteProjectTpaIntegrationResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest ThirdPartyAuthOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetProjectTpaIntegrationResponse parses an HTTP response from a V1GetProjectTpaIntegrationWithResponse call -func ParseV1GetProjectTpaIntegrationResponse(rsp *http.Response) (*V1GetProjectTpaIntegrationResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetProjectTpaIntegrationResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest ThirdPartyAuthOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetProjectPgbouncerConfigResponse parses an HTTP response from a V1GetProjectPgbouncerConfigWithResponse call -func ParseV1GetProjectPgbouncerConfigResponse(rsp *http.Response) (*V1GetProjectPgbouncerConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetProjectPgbouncerConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest V1PgbouncerConfigResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetPoolerConfigResponse parses an HTTP response from a V1GetPoolerConfigWithResponse call -func ParseV1GetPoolerConfigResponse(rsp *http.Response) (*V1GetPoolerConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetPoolerConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest []SupavisorConfigResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1UpdatePoolerConfigResponse parses an HTTP response from a V1UpdatePoolerConfigWithResponse call -func ParseV1UpdatePoolerConfigResponse(rsp *http.Response) (*V1UpdatePoolerConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdatePoolerConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest UpdateSupavisorConfigResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetPostgresConfigResponse parses an HTTP response from a V1GetPostgresConfigWithResponse call -func ParseV1GetPostgresConfigResponse(rsp *http.Response) (*V1GetPostgresConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetPostgresConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest PostgresConfigResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1UpdatePostgresConfigResponse parses an HTTP response from a V1UpdatePostgresConfigWithResponse call -func ParseV1UpdatePostgresConfigResponse(rsp *http.Response) (*V1UpdatePostgresConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdatePostgresConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest PostgresConfigResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetDatabaseDiskResponse parses an HTTP response from a V1GetDatabaseDiskWithResponse call -func ParseV1GetDatabaseDiskResponse(rsp *http.Response) (*V1GetDatabaseDiskResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetDatabaseDiskResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest DiskResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1ModifyDatabaseDiskResponse parses an HTTP response from a V1ModifyDatabaseDiskWithResponse call -func ParseV1ModifyDatabaseDiskResponse(rsp *http.Response) (*V1ModifyDatabaseDiskResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ModifyDatabaseDiskResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1GetProjectDiskAutoscaleConfigResponse parses an HTTP response from a V1GetProjectDiskAutoscaleConfigWithResponse call -func ParseV1GetProjectDiskAutoscaleConfigResponse(rsp *http.Response) (*V1GetProjectDiskAutoscaleConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetProjectDiskAutoscaleConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest DiskAutoscaleConfigOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetDiskUtilizationResponse parses an HTTP response from a V1GetDiskUtilizationWithResponse call -func ParseV1GetDiskUtilizationResponse(rsp *http.Response) (*V1GetDiskUtilizationResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetDiskUtilizationResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest DiskUtilMetricsResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetRealtimeConfigResponse parses an HTTP response from a V1GetRealtimeConfigWithResponse call -func ParseV1GetRealtimeConfigResponse(rsp *http.Response) (*V1GetRealtimeConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetRealtimeConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest RealtimeConfigResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1UpdateRealtimeConfigResponse parses an HTTP response from a V1UpdateRealtimeConfigWithResponse call -func ParseV1UpdateRealtimeConfigResponse(rsp *http.Response) (*V1UpdateRealtimeConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdateRealtimeConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1ShutdownRealtimeResponse parses an HTTP response from a V1ShutdownRealtimeWithResponse call -func ParseV1ShutdownRealtimeResponse(rsp *http.Response) (*V1ShutdownRealtimeResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ShutdownRealtimeResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1GetStorageConfigResponse parses an HTTP response from a V1GetStorageConfigWithResponse call -func ParseV1GetStorageConfigResponse(rsp *http.Response) (*V1GetStorageConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetStorageConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest StorageConfigResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1UpdateStorageConfigResponse parses an HTTP response from a V1UpdateStorageConfigWithResponse call -func ParseV1UpdateStorageConfigResponse(rsp *http.Response) (*V1UpdateStorageConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdateStorageConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1DeleteHostnameConfigResponse parses an HTTP response from a V1DeleteHostnameConfigWithResponse call -func ParseV1DeleteHostnameConfigResponse(rsp *http.Response) (*V1DeleteHostnameConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1DeleteHostnameConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1GetHostnameConfigResponse parses an HTTP response from a V1GetHostnameConfigWithResponse call -func ParseV1GetHostnameConfigResponse(rsp *http.Response) (*V1GetHostnameConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetHostnameConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest UpdateCustomHostnameResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1ActivateCustomHostnameResponse parses an HTTP response from a V1ActivateCustomHostnameWithResponse call -func ParseV1ActivateCustomHostnameResponse(rsp *http.Response) (*V1ActivateCustomHostnameResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ActivateCustomHostnameResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest UpdateCustomHostnameResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1UpdateHostnameConfigResponse parses an HTTP response from a V1UpdateHostnameConfigWithResponse call -func ParseV1UpdateHostnameConfigResponse(rsp *http.Response) (*V1UpdateHostnameConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdateHostnameConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest UpdateCustomHostnameResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1VerifyDnsConfigResponse parses an HTTP response from a V1VerifyDnsConfigWithResponse call -func ParseV1VerifyDnsConfigResponse(rsp *http.Response) (*V1VerifyDnsConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1VerifyDnsConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest UpdateCustomHostnameResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1ListAllBackupsResponse parses an HTTP response from a V1ListAllBackupsWithResponse call -func ParseV1ListAllBackupsResponse(rsp *http.Response) (*V1ListAllBackupsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ListAllBackupsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest V1BackupsResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1RestorePhysicalBackupResponse parses an HTTP response from a V1RestorePhysicalBackupWithResponse call -func ParseV1RestorePhysicalBackupResponse(rsp *http.Response) (*V1RestorePhysicalBackupResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1RestorePhysicalBackupResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1RestorePitrBackupResponse parses an HTTP response from a V1RestorePitrBackupWithResponse call -func ParseV1RestorePitrBackupResponse(rsp *http.Response) (*V1RestorePitrBackupResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1RestorePitrBackupResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1GetRestorePointResponse parses an HTTP response from a V1GetRestorePointWithResponse call -func ParseV1GetRestorePointResponse(rsp *http.Response) (*V1GetRestorePointResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetRestorePointResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest V1RestorePointResponse - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1CreateRestorePointResponse parses an HTTP response from a V1CreateRestorePointWithResponse call -func ParseV1CreateRestorePointResponse(rsp *http.Response) (*V1CreateRestorePointResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1CreateRestorePointResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest V1RestorePointResponse - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1GetBackupScheduleResponse parses an HTTP response from a V1GetBackupScheduleWithResponse call -func ParseV1GetBackupScheduleResponse(rsp *http.Response) (*V1GetBackupScheduleResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetBackupScheduleResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest V1BackupScheduleResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 402: - var dest PlanGateErrorBody - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON402 = &dest - - } - - return response, nil -} - -// ParseV1UpdateBackupScheduleResponse parses an HTTP response from a V1UpdateBackupScheduleWithResponse call -func ParseV1UpdateBackupScheduleResponse(rsp *http.Response) (*V1UpdateBackupScheduleResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdateBackupScheduleResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest V1BackupScheduleResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 402: - var dest PlanGateErrorBody - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON402 = &dest - - } - - return response, nil -} - -// ParseV1UndoResponse parses an HTTP response from a V1UndoWithResponse call -func ParseV1UndoResponse(rsp *http.Response) (*V1UndoResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UndoResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1GetDatabaseMetadataResponse parses an HTTP response from a V1GetDatabaseMetadataWithResponse call -func ParseV1GetDatabaseMetadataResponse(rsp *http.Response) (*V1GetDatabaseMetadataResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetDatabaseMetadataResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest GetProjectDbMetadataResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetJitAccessResponse parses an HTTP response from a V1GetJitAccessWithResponse call -func ParseV1GetJitAccessResponse(rsp *http.Response) (*V1GetJitAccessResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetJitAccessResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest JitAccessResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1AuthorizeJitAccessResponse parses an HTTP response from a V1AuthorizeJitAccessWithResponse call -func ParseV1AuthorizeJitAccessResponse(rsp *http.Response) (*V1AuthorizeJitAccessResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1AuthorizeJitAccessResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest JitAuthorizeAccessResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1UpdateJitAccessResponse parses an HTTP response from a V1UpdateJitAccessWithResponse call -func ParseV1UpdateJitAccessResponse(rsp *http.Response) (*V1UpdateJitAccessResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdateJitAccessResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest JitAccessResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1InviteExternalJitAccessResponse parses an HTTP response from a V1InviteExternalJitAccessWithResponse call -func ParseV1InviteExternalJitAccessResponse(rsp *http.Response) (*V1InviteExternalJitAccessResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1InviteExternalJitAccessResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest InviteExternalUserJitResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1AcceptInviteExternalJitAccessResponse parses an HTTP response from a V1AcceptInviteExternalJitAccessWithResponse call -func ParseV1AcceptInviteExternalJitAccessResponse(rsp *http.Response) (*V1AcceptInviteExternalJitAccessResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1AcceptInviteExternalJitAccessResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest JitAccessResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1DeleteInviteExternalJitAccessResponse parses an HTTP response from a V1DeleteInviteExternalJitAccessWithResponse call -func ParseV1DeleteInviteExternalJitAccessResponse(rsp *http.Response) (*V1DeleteInviteExternalJitAccessResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1DeleteInviteExternalJitAccessResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1ListJitAccessResponse parses an HTTP response from a V1ListJitAccessWithResponse call -func ParseV1ListJitAccessResponse(rsp *http.Response) (*V1ListJitAccessResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ListJitAccessResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest JitListAccessResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1DeleteJitAccessResponse parses an HTTP response from a V1DeleteJitAccessWithResponse call -func ParseV1DeleteJitAccessResponse(rsp *http.Response) (*V1DeleteJitAccessResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1DeleteJitAccessResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1RollbackMigrationsResponse parses an HTTP response from a V1RollbackMigrationsWithResponse call -func ParseV1RollbackMigrationsResponse(rsp *http.Response) (*V1RollbackMigrationsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1RollbackMigrationsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1ListMigrationHistoryResponse parses an HTTP response from a V1ListMigrationHistoryWithResponse call -func ParseV1ListMigrationHistoryResponse(rsp *http.Response) (*V1ListMigrationHistoryResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ListMigrationHistoryResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest V1ListMigrationsResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1ApplyAMigrationResponse parses an HTTP response from a V1ApplyAMigrationWithResponse call -func ParseV1ApplyAMigrationResponse(rsp *http.Response) (*V1ApplyAMigrationResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ApplyAMigrationResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1UpsertAMigrationResponse parses an HTTP response from a V1UpsertAMigrationWithResponse call -func ParseV1UpsertAMigrationResponse(rsp *http.Response) (*V1UpsertAMigrationResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpsertAMigrationResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1GetAMigrationResponse parses an HTTP response from a V1GetAMigrationWithResponse call -func ParseV1GetAMigrationResponse(rsp *http.Response) (*V1GetAMigrationResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetAMigrationResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest V1GetMigrationResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1PatchAMigrationResponse parses an HTTP response from a V1PatchAMigrationWithResponse call -func ParseV1PatchAMigrationResponse(rsp *http.Response) (*V1PatchAMigrationResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1PatchAMigrationResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1GetDatabaseOpenapiResponse parses an HTTP response from a V1GetDatabaseOpenapiWithResponse call -func ParseV1GetDatabaseOpenapiResponse(rsp *http.Response) (*V1GetDatabaseOpenapiResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetDatabaseOpenapiResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest map[string]interface{} - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1UpdateDatabasePasswordResponse parses an HTTP response from a V1UpdateDatabasePasswordWithResponse call -func ParseV1UpdateDatabasePasswordResponse(rsp *http.Response) (*V1UpdateDatabasePasswordResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdateDatabasePasswordResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest V1UpdatePasswordResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1RunAQueryResponse parses an HTTP response from a V1RunAQueryWithResponse call -func ParseV1RunAQueryResponse(rsp *http.Response) (*V1RunAQueryResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1RunAQueryResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1ReadOnlyQueryResponse parses an HTTP response from a V1ReadOnlyQueryWithResponse call -func ParseV1ReadOnlyQueryResponse(rsp *http.Response) (*V1ReadOnlyQueryResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ReadOnlyQueryResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1EnableDatabaseWebhookResponse parses an HTTP response from a V1EnableDatabaseWebhookWithResponse call -func ParseV1EnableDatabaseWebhookResponse(rsp *http.Response) (*V1EnableDatabaseWebhookResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1EnableDatabaseWebhookResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1ListAllFunctionsResponse parses an HTTP response from a V1ListAllFunctionsWithResponse call -func ParseV1ListAllFunctionsResponse(rsp *http.Response) (*V1ListAllFunctionsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ListAllFunctionsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest []FunctionResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1CreateAFunctionResponse parses an HTTP response from a V1CreateAFunctionWithResponse call -func ParseV1CreateAFunctionResponse(rsp *http.Response) (*V1CreateAFunctionResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1CreateAFunctionResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest FunctionResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1BulkUpdateFunctionsResponse parses an HTTP response from a V1BulkUpdateFunctionsWithResponse call -func ParseV1BulkUpdateFunctionsResponse(rsp *http.Response) (*V1BulkUpdateFunctionsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1BulkUpdateFunctionsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest BulkUpdateFunctionResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1DeployAFunctionResponse parses an HTTP response from a V1DeployAFunctionWithResponse call -func ParseV1DeployAFunctionResponse(rsp *http.Response) (*V1DeployAFunctionResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1DeployAFunctionResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest DeployFunctionResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1DeleteAFunctionResponse parses an HTTP response from a V1DeleteAFunctionWithResponse call -func ParseV1DeleteAFunctionResponse(rsp *http.Response) (*V1DeleteAFunctionResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1DeleteAFunctionResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1GetAFunctionResponse parses an HTTP response from a V1GetAFunctionWithResponse call -func ParseV1GetAFunctionResponse(rsp *http.Response) (*V1GetAFunctionResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetAFunctionResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest FunctionSlugResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1UpdateAFunctionResponse parses an HTTP response from a V1UpdateAFunctionWithResponse call -func ParseV1UpdateAFunctionResponse(rsp *http.Response) (*V1UpdateAFunctionResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdateAFunctionResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest FunctionSlugResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetAFunctionBodyResponse parses an HTTP response from a V1GetAFunctionBodyWithResponse call -func ParseV1GetAFunctionBodyResponse(rsp *http.Response) (*V1GetAFunctionBodyResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetAFunctionBodyResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest StreamableFile - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetServicesHealthResponse parses an HTTP response from a V1GetServicesHealthWithResponse call -func ParseV1GetServicesHealthResponse(rsp *http.Response) (*V1GetServicesHealthResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetServicesHealthResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest []V1ServiceHealthResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetJitAccessConfigResponse parses an HTTP response from a V1GetJitAccessConfigWithResponse call -func ParseV1GetJitAccessConfigResponse(rsp *http.Response) (*V1GetJitAccessConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetJitAccessConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest V1GetJitAccessConfig200JSONResponseBody - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1UpdateJitAccessConfigResponse parses an HTTP response from a V1UpdateJitAccessConfigWithResponse call -func ParseV1UpdateJitAccessConfigResponse(rsp *http.Response) (*V1UpdateJitAccessConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdateJitAccessConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest V1UpdateJitAccessConfig200JSONResponseBody - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1DeleteNetworkBansResponse parses an HTTP response from a V1DeleteNetworkBansWithResponse call -func ParseV1DeleteNetworkBansResponse(rsp *http.Response) (*V1DeleteNetworkBansResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1DeleteNetworkBansResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1ListAllNetworkBansResponse parses an HTTP response from a V1ListAllNetworkBansWithResponse call -func ParseV1ListAllNetworkBansResponse(rsp *http.Response) (*V1ListAllNetworkBansResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ListAllNetworkBansResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest NetworkBanResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1ListAllNetworkBansEnrichedResponse parses an HTTP response from a V1ListAllNetworkBansEnrichedWithResponse call -func ParseV1ListAllNetworkBansEnrichedResponse(rsp *http.Response) (*V1ListAllNetworkBansEnrichedResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ListAllNetworkBansEnrichedResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest NetworkBanResponseEnrichedOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1GetNetworkRestrictionsResponse parses an HTTP response from a V1GetNetworkRestrictionsWithResponse call -func ParseV1GetNetworkRestrictionsResponse(rsp *http.Response) (*V1GetNetworkRestrictionsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetNetworkRestrictionsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest NetworkRestrictionsResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1PatchNetworkRestrictionsResponse parses an HTTP response from a V1PatchNetworkRestrictionsWithResponse call -func ParseV1PatchNetworkRestrictionsResponse(rsp *http.Response) (*V1PatchNetworkRestrictionsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1PatchNetworkRestrictionsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest NetworkRestrictionsV2ResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1UpdateNetworkRestrictionsResponse parses an HTTP response from a V1UpdateNetworkRestrictionsWithResponse call -func ParseV1UpdateNetworkRestrictionsResponse(rsp *http.Response) (*V1UpdateNetworkRestrictionsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdateNetworkRestrictionsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest NetworkRestrictionsResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1PauseAProjectResponse parses an HTTP response from a V1PauseAProjectWithResponse call -func ParseV1PauseAProjectResponse(rsp *http.Response) (*V1PauseAProjectResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1PauseAProjectResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1GetPgsodiumConfigResponse parses an HTTP response from a V1GetPgsodiumConfigWithResponse call -func ParseV1GetPgsodiumConfigResponse(rsp *http.Response) (*V1GetPgsodiumConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetPgsodiumConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest PgsodiumConfigResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1UpdatePgsodiumConfigResponse parses an HTTP response from a V1UpdatePgsodiumConfigWithResponse call -func ParseV1UpdatePgsodiumConfigResponse(rsp *http.Response) (*V1UpdatePgsodiumConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdatePgsodiumConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest PgsodiumConfigResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetPostgrestServiceConfigResponse parses an HTTP response from a V1GetPostgrestServiceConfigWithResponse call -func ParseV1GetPostgrestServiceConfigResponse(rsp *http.Response) (*V1GetPostgrestServiceConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetPostgrestServiceConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest PostgrestConfigWithJWTSecretResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1UpdatePostgrestServiceConfigResponse parses an HTTP response from a V1UpdatePostgrestServiceConfigWithResponse call -func ParseV1UpdatePostgrestServiceConfigResponse(rsp *http.Response) (*V1UpdatePostgrestServiceConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdatePostgrestServiceConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest V1PostgrestConfigResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1RemoveAReadReplicaResponse parses an HTTP response from a V1RemoveAReadReplicaWithResponse call -func ParseV1RemoveAReadReplicaResponse(rsp *http.Response) (*V1RemoveAReadReplicaResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1RemoveAReadReplicaResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1SetupAReadReplicaResponse parses an HTTP response from a V1SetupAReadReplicaWithResponse call -func ParseV1SetupAReadReplicaResponse(rsp *http.Response) (*V1SetupAReadReplicaResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1SetupAReadReplicaResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 402: - var dest PlanGateErrorBody - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON402 = &dest - - } - - return response, nil -} - -// ParseV1GetReadonlyModeStatusResponse parses an HTTP response from a V1GetReadonlyModeStatusWithResponse call -func ParseV1GetReadonlyModeStatusResponse(rsp *http.Response) (*V1GetReadonlyModeStatusResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetReadonlyModeStatusResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest ReadOnlyStatusResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1DisableReadonlyModeTemporarilyResponse parses an HTTP response from a V1DisableReadonlyModeTemporarilyWithResponse call -func ParseV1DisableReadonlyModeTemporarilyResponse(rsp *http.Response) (*V1DisableReadonlyModeTemporarilyResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1DisableReadonlyModeTemporarilyResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1RestartAProjectResponse parses an HTTP response from a V1RestartAProjectWithResponse call -func ParseV1RestartAProjectResponse(rsp *http.Response) (*V1RestartAProjectResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1RestartAProjectResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1ListAvailableRestoreVersionsResponse parses an HTTP response from a V1ListAvailableRestoreVersionsWithResponse call -func ParseV1ListAvailableRestoreVersionsResponse(rsp *http.Response) (*V1ListAvailableRestoreVersionsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ListAvailableRestoreVersionsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest GetProjectAvailableRestoreVersionsResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1RestoreAProjectResponse parses an HTTP response from a V1RestoreAProjectWithResponse call -func ParseV1RestoreAProjectResponse(rsp *http.Response) (*V1RestoreAProjectResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1RestoreAProjectResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1CancelAProjectRestorationResponse parses an HTTP response from a V1CancelAProjectRestorationWithResponse call -func ParseV1CancelAProjectRestorationResponse(rsp *http.Response) (*V1CancelAProjectRestorationResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1CancelAProjectRestorationResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1BulkDeleteSecretsResponse parses an HTTP response from a V1BulkDeleteSecretsWithResponse call -func ParseV1BulkDeleteSecretsResponse(rsp *http.Response) (*V1BulkDeleteSecretsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1BulkDeleteSecretsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1ListAllSecretsResponse parses an HTTP response from a V1ListAllSecretsWithResponse call -func ParseV1ListAllSecretsResponse(rsp *http.Response) (*V1ListAllSecretsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ListAllSecretsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest []SecretResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1BulkCreateSecretsResponse parses an HTTP response from a V1BulkCreateSecretsWithResponse call -func ParseV1BulkCreateSecretsResponse(rsp *http.Response) (*V1BulkCreateSecretsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1BulkCreateSecretsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1GetSslEnforcementConfigResponse parses an HTTP response from a V1GetSslEnforcementConfigWithResponse call -func ParseV1GetSslEnforcementConfigResponse(rsp *http.Response) (*V1GetSslEnforcementConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetSslEnforcementConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest SslEnforcementResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1UpdateSslEnforcementConfigResponse parses an HTTP response from a V1UpdateSslEnforcementConfigWithResponse call -func ParseV1UpdateSslEnforcementConfigResponse(rsp *http.Response) (*V1UpdateSslEnforcementConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpdateSslEnforcementConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest SslEnforcementResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1ListAllBucketsResponse parses an HTTP response from a V1ListAllBucketsWithResponse call -func ParseV1ListAllBucketsResponse(rsp *http.Response) (*V1ListAllBucketsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ListAllBucketsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest []V1StorageBucketResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GenerateTypescriptTypesResponse parses an HTTP response from a V1GenerateTypescriptTypesWithResponse call -func ParseV1GenerateTypescriptTypesResponse(rsp *http.Response) (*V1GenerateTypescriptTypesResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GenerateTypescriptTypesResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest TypescriptResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1UpgradePostgresVersionResponse parses an HTTP response from a V1UpgradePostgresVersionWithResponse call -func ParseV1UpgradePostgresVersionResponse(rsp *http.Response) (*V1UpgradePostgresVersionResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1UpgradePostgresVersionResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest ProjectUpgradeInitiateResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - } - - return response, nil -} - -// ParseV1GetPostgresUpgradeEligibilityResponse parses an HTTP response from a V1GetPostgresUpgradeEligibilityWithResponse call -func ParseV1GetPostgresUpgradeEligibilityResponse(rsp *http.Response) (*V1GetPostgresUpgradeEligibilityResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetPostgresUpgradeEligibilityResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest ProjectUpgradeEligibilityResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetPostgresUpgradeStatusResponse parses an HTTP response from a V1GetPostgresUpgradeStatusWithResponse call -func ParseV1GetPostgresUpgradeStatusResponse(rsp *http.Response) (*V1GetPostgresUpgradeStatusResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetPostgresUpgradeStatusResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest DatabaseUpgradeStatusResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1DeactivateVanitySubdomainConfigResponse parses an HTTP response from a V1DeactivateVanitySubdomainConfigWithResponse call -func ParseV1DeactivateVanitySubdomainConfigResponse(rsp *http.Response) (*V1DeactivateVanitySubdomainConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1DeactivateVanitySubdomainConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - return response, nil -} - -// ParseV1GetVanitySubdomainConfigResponse parses an HTTP response from a V1GetVanitySubdomainConfigWithResponse call -func ParseV1GetVanitySubdomainConfigResponse(rsp *http.Response) (*V1GetVanitySubdomainConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetVanitySubdomainConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest VanitySubdomainConfigResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: - var dest PlanGateErrorBody - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON400 = &dest - - } - - return response, nil -} - -// ParseV1ActivateVanitySubdomainConfigResponse parses an HTTP response from a V1ActivateVanitySubdomainConfigWithResponse call -func ParseV1ActivateVanitySubdomainConfigResponse(rsp *http.Response) (*V1ActivateVanitySubdomainConfigResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ActivateVanitySubdomainConfigResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest ActivateVanitySubdomainResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: - var dest PlanGateErrorBody - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON400 = &dest - - } - - return response, nil -} - -// ParseV1CheckVanitySubdomainAvailabilityResponse parses an HTTP response from a V1CheckVanitySubdomainAvailabilityWithResponse call -func ParseV1CheckVanitySubdomainAvailabilityResponse(rsp *http.Response) (*V1CheckVanitySubdomainAvailabilityResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1CheckVanitySubdomainAvailabilityResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest SubdomainAvailabilityResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: - var dest PlanGateErrorBody - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON400 = &dest - - } - - return response, nil -} - -// ParseV1ListAllSnippetsResponse parses an HTTP response from a V1ListAllSnippetsWithResponse call -func ParseV1ListAllSnippetsResponse(rsp *http.Response) (*V1ListAllSnippetsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1ListAllSnippetsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest SnippetListOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseV1GetASnippetResponse parses an HTTP response from a V1GetASnippetWithResponse call -func ParseV1GetASnippetResponse(rsp *http.Response) (*V1GetASnippetResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &V1GetASnippetResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest SnippetResponseOutput - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} diff --git a/apps/cli-go/pkg/api/types.cfg.yaml b/apps/cli-go/pkg/api/types.cfg.yaml deleted file mode 100755 index 128f4a387b..0000000000 --- a/apps/cli-go/pkg/api/types.cfg.yaml +++ /dev/null @@ -1,8 +0,0 @@ -package: api -generate: - models: true -output-options: - nullable-type: true - overlay: - path: api/overlay.yaml -output: pkg/api/types.gen.go diff --git a/apps/cli-go/pkg/api/types.gen.go b/apps/cli-go/pkg/api/types.gen.go deleted file mode 100644 index 54523885fa..0000000000 --- a/apps/cli-go/pkg/api/types.gen.go +++ /dev/null @@ -1,11711 +0,0 @@ -// Package api provides primitives to interact with the openapi HTTP API. -// -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.7.1 DO NOT EDIT. -package api - -import ( - "encoding/json" - "time" - - "github.com/oapi-codegen/nullable" - "github.com/oapi-codegen/runtime" - openapi_types "github.com/oapi-codegen/runtime/types" -) - -const ( - BearerScopes bearerContextKey = "bearer.Scopes" -) - -// Defines values for ActionRunResponseOutputRunStepsName. -const ( - ActionRunResponseOutputRunStepsNameClone ActionRunResponseOutputRunStepsName = "clone" - ActionRunResponseOutputRunStepsNameConfigure ActionRunResponseOutputRunStepsName = "configure" - ActionRunResponseOutputRunStepsNameDeploy ActionRunResponseOutputRunStepsName = "deploy" - ActionRunResponseOutputRunStepsNameHealth ActionRunResponseOutputRunStepsName = "health" - ActionRunResponseOutputRunStepsNameMigrate ActionRunResponseOutputRunStepsName = "migrate" - ActionRunResponseOutputRunStepsNamePull ActionRunResponseOutputRunStepsName = "pull" - ActionRunResponseOutputRunStepsNameSeed ActionRunResponseOutputRunStepsName = "seed" -) - -// Valid indicates whether the value is a known member of the ActionRunResponseOutputRunStepsName enum. -func (e ActionRunResponseOutputRunStepsName) Valid() bool { - switch e { - case ActionRunResponseOutputRunStepsNameClone: - return true - case ActionRunResponseOutputRunStepsNameConfigure: - return true - case ActionRunResponseOutputRunStepsNameDeploy: - return true - case ActionRunResponseOutputRunStepsNameHealth: - return true - case ActionRunResponseOutputRunStepsNameMigrate: - return true - case ActionRunResponseOutputRunStepsNamePull: - return true - case ActionRunResponseOutputRunStepsNameSeed: - return true - default: - return false - } -} - -// Defines values for ActionRunResponseOutputRunStepsStatus. -const ( - ActionRunResponseOutputRunStepsStatusCREATED ActionRunResponseOutputRunStepsStatus = "CREATED" - ActionRunResponseOutputRunStepsStatusDEAD ActionRunResponseOutputRunStepsStatus = "DEAD" - ActionRunResponseOutputRunStepsStatusEXITED ActionRunResponseOutputRunStepsStatus = "EXITED" - ActionRunResponseOutputRunStepsStatusPAUSED ActionRunResponseOutputRunStepsStatus = "PAUSED" - ActionRunResponseOutputRunStepsStatusREMOVING ActionRunResponseOutputRunStepsStatus = "REMOVING" - ActionRunResponseOutputRunStepsStatusRESTARTING ActionRunResponseOutputRunStepsStatus = "RESTARTING" - ActionRunResponseOutputRunStepsStatusRUNNING ActionRunResponseOutputRunStepsStatus = "RUNNING" -) - -// Valid indicates whether the value is a known member of the ActionRunResponseOutputRunStepsStatus enum. -func (e ActionRunResponseOutputRunStepsStatus) Valid() bool { - switch e { - case ActionRunResponseOutputRunStepsStatusCREATED: - return true - case ActionRunResponseOutputRunStepsStatusDEAD: - return true - case ActionRunResponseOutputRunStepsStatusEXITED: - return true - case ActionRunResponseOutputRunStepsStatusPAUSED: - return true - case ActionRunResponseOutputRunStepsStatusREMOVING: - return true - case ActionRunResponseOutputRunStepsStatusRESTARTING: - return true - case ActionRunResponseOutputRunStepsStatusRUNNING: - return true - default: - return false - } -} - -// Defines values for ApiKeyResponseOutputType. -const ( - ApiKeyResponseOutputTypeLegacy ApiKeyResponseOutputType = "legacy" - ApiKeyResponseOutputTypeLessThannil ApiKeyResponseOutputType = "" - ApiKeyResponseOutputTypePublishable ApiKeyResponseOutputType = "publishable" - ApiKeyResponseOutputTypeSecret ApiKeyResponseOutputType = "secret" -) - -// Valid indicates whether the value is a known member of the ApiKeyResponseOutputType enum. -func (e ApiKeyResponseOutputType) Valid() bool { - switch e { - case ApiKeyResponseOutputTypeLegacy: - return true - case ApiKeyResponseOutputTypeLessThannil: - return true - case ApiKeyResponseOutputTypePublishable: - return true - case ApiKeyResponseOutputTypeSecret: - return true - default: - return false - } -} - -// Defines values for ApplyProjectAddonBodyAddonType. -const ( - ApplyProjectAddonBodyAddonTypeAuthMfaPhone ApplyProjectAddonBodyAddonType = "auth_mfa_phone" - ApplyProjectAddonBodyAddonTypeAuthMfaWebAuthn ApplyProjectAddonBodyAddonType = "auth_mfa_web_authn" - ApplyProjectAddonBodyAddonTypeComputeInstance ApplyProjectAddonBodyAddonType = "compute_instance" - ApplyProjectAddonBodyAddonTypeCustomDomain ApplyProjectAddonBodyAddonType = "custom_domain" - ApplyProjectAddonBodyAddonTypeEtlPipeline ApplyProjectAddonBodyAddonType = "etl_pipeline" - ApplyProjectAddonBodyAddonTypeIpv4 ApplyProjectAddonBodyAddonType = "ipv4" - ApplyProjectAddonBodyAddonTypeLogDrain ApplyProjectAddonBodyAddonType = "log_drain" - ApplyProjectAddonBodyAddonTypePitr ApplyProjectAddonBodyAddonType = "pitr" -) - -// Valid indicates whether the value is a known member of the ApplyProjectAddonBodyAddonType enum. -func (e ApplyProjectAddonBodyAddonType) Valid() bool { - switch e { - case ApplyProjectAddonBodyAddonTypeAuthMfaPhone: - return true - case ApplyProjectAddonBodyAddonTypeAuthMfaWebAuthn: - return true - case ApplyProjectAddonBodyAddonTypeComputeInstance: - return true - case ApplyProjectAddonBodyAddonTypeCustomDomain: - return true - case ApplyProjectAddonBodyAddonTypeEtlPipeline: - return true - case ApplyProjectAddonBodyAddonTypeIpv4: - return true - case ApplyProjectAddonBodyAddonTypeLogDrain: - return true - case ApplyProjectAddonBodyAddonTypePitr: - return true - default: - return false - } -} - -// Defines values for ApplyProjectAddonBodyAddonVariant0. -const ( - ApplyProjectAddonBodyAddonVariant0Ci12xlarge ApplyProjectAddonBodyAddonVariant0 = "ci_12xlarge" - ApplyProjectAddonBodyAddonVariant0Ci16xlarge ApplyProjectAddonBodyAddonVariant0 = "ci_16xlarge" - ApplyProjectAddonBodyAddonVariant0Ci24xlarge ApplyProjectAddonBodyAddonVariant0 = "ci_24xlarge" - ApplyProjectAddonBodyAddonVariant0Ci24xlargeHighMemory ApplyProjectAddonBodyAddonVariant0 = "ci_24xlarge_high_memory" - ApplyProjectAddonBodyAddonVariant0Ci24xlargeOptimizedCpu ApplyProjectAddonBodyAddonVariant0 = "ci_24xlarge_optimized_cpu" - ApplyProjectAddonBodyAddonVariant0Ci24xlargeOptimizedMemory ApplyProjectAddonBodyAddonVariant0 = "ci_24xlarge_optimized_memory" - ApplyProjectAddonBodyAddonVariant0Ci2xlarge ApplyProjectAddonBodyAddonVariant0 = "ci_2xlarge" - ApplyProjectAddonBodyAddonVariant0Ci48xlarge ApplyProjectAddonBodyAddonVariant0 = "ci_48xlarge" - ApplyProjectAddonBodyAddonVariant0Ci48xlargeHighMemory ApplyProjectAddonBodyAddonVariant0 = "ci_48xlarge_high_memory" - ApplyProjectAddonBodyAddonVariant0Ci48xlargeOptimizedCpu ApplyProjectAddonBodyAddonVariant0 = "ci_48xlarge_optimized_cpu" - ApplyProjectAddonBodyAddonVariant0Ci48xlargeOptimizedMemory ApplyProjectAddonBodyAddonVariant0 = "ci_48xlarge_optimized_memory" - ApplyProjectAddonBodyAddonVariant0Ci4xlarge ApplyProjectAddonBodyAddonVariant0 = "ci_4xlarge" - ApplyProjectAddonBodyAddonVariant0Ci8xlarge ApplyProjectAddonBodyAddonVariant0 = "ci_8xlarge" - ApplyProjectAddonBodyAddonVariant0CiLarge ApplyProjectAddonBodyAddonVariant0 = "ci_large" - ApplyProjectAddonBodyAddonVariant0CiMedium ApplyProjectAddonBodyAddonVariant0 = "ci_medium" - ApplyProjectAddonBodyAddonVariant0CiMicro ApplyProjectAddonBodyAddonVariant0 = "ci_micro" - ApplyProjectAddonBodyAddonVariant0CiSmall ApplyProjectAddonBodyAddonVariant0 = "ci_small" - ApplyProjectAddonBodyAddonVariant0CiXlarge ApplyProjectAddonBodyAddonVariant0 = "ci_xlarge" -) - -// Valid indicates whether the value is a known member of the ApplyProjectAddonBodyAddonVariant0 enum. -func (e ApplyProjectAddonBodyAddonVariant0) Valid() bool { - switch e { - case ApplyProjectAddonBodyAddonVariant0Ci12xlarge: - return true - case ApplyProjectAddonBodyAddonVariant0Ci16xlarge: - return true - case ApplyProjectAddonBodyAddonVariant0Ci24xlarge: - return true - case ApplyProjectAddonBodyAddonVariant0Ci24xlargeHighMemory: - return true - case ApplyProjectAddonBodyAddonVariant0Ci24xlargeOptimizedCpu: - return true - case ApplyProjectAddonBodyAddonVariant0Ci24xlargeOptimizedMemory: - return true - case ApplyProjectAddonBodyAddonVariant0Ci2xlarge: - return true - case ApplyProjectAddonBodyAddonVariant0Ci48xlarge: - return true - case ApplyProjectAddonBodyAddonVariant0Ci48xlargeHighMemory: - return true - case ApplyProjectAddonBodyAddonVariant0Ci48xlargeOptimizedCpu: - return true - case ApplyProjectAddonBodyAddonVariant0Ci48xlargeOptimizedMemory: - return true - case ApplyProjectAddonBodyAddonVariant0Ci4xlarge: - return true - case ApplyProjectAddonBodyAddonVariant0Ci8xlarge: - return true - case ApplyProjectAddonBodyAddonVariant0CiLarge: - return true - case ApplyProjectAddonBodyAddonVariant0CiMedium: - return true - case ApplyProjectAddonBodyAddonVariant0CiMicro: - return true - case ApplyProjectAddonBodyAddonVariant0CiSmall: - return true - case ApplyProjectAddonBodyAddonVariant0CiXlarge: - return true - default: - return false - } -} - -// Defines values for ApplyProjectAddonBodyAddonVariant1. -const ( - ApplyProjectAddonBodyAddonVariant1CdDefault ApplyProjectAddonBodyAddonVariant1 = "cd_default" -) - -// Valid indicates whether the value is a known member of the ApplyProjectAddonBodyAddonVariant1 enum. -func (e ApplyProjectAddonBodyAddonVariant1) Valid() bool { - switch e { - case ApplyProjectAddonBodyAddonVariant1CdDefault: - return true - default: - return false - } -} - -// Defines values for ApplyProjectAddonBodyAddonVariant2. -const ( - ApplyProjectAddonBodyAddonVariant2Pitr14 ApplyProjectAddonBodyAddonVariant2 = "pitr_14" - ApplyProjectAddonBodyAddonVariant2Pitr28 ApplyProjectAddonBodyAddonVariant2 = "pitr_28" - ApplyProjectAddonBodyAddonVariant2Pitr7 ApplyProjectAddonBodyAddonVariant2 = "pitr_7" -) - -// Valid indicates whether the value is a known member of the ApplyProjectAddonBodyAddonVariant2 enum. -func (e ApplyProjectAddonBodyAddonVariant2) Valid() bool { - switch e { - case ApplyProjectAddonBodyAddonVariant2Pitr14: - return true - case ApplyProjectAddonBodyAddonVariant2Pitr28: - return true - case ApplyProjectAddonBodyAddonVariant2Pitr7: - return true - default: - return false - } -} - -// Defines values for ApplyProjectAddonBodyAddonVariant3. -const ( - ApplyProjectAddonBodyAddonVariant3Ipv4Default ApplyProjectAddonBodyAddonVariant3 = "ipv4_default" -) - -// Valid indicates whether the value is a known member of the ApplyProjectAddonBodyAddonVariant3 enum. -func (e ApplyProjectAddonBodyAddonVariant3) Valid() bool { - switch e { - case ApplyProjectAddonBodyAddonVariant3Ipv4Default: - return true - default: - return false - } -} - -// Defines values for AuthConfigResponseOutputDbMaxPoolSizeUnit. -const ( - AuthConfigResponseOutputDbMaxPoolSizeUnitConnections AuthConfigResponseOutputDbMaxPoolSizeUnit = "connections" - AuthConfigResponseOutputDbMaxPoolSizeUnitLessThannil AuthConfigResponseOutputDbMaxPoolSizeUnit = "" - AuthConfigResponseOutputDbMaxPoolSizeUnitPercent AuthConfigResponseOutputDbMaxPoolSizeUnit = "percent" -) - -// Valid indicates whether the value is a known member of the AuthConfigResponseOutputDbMaxPoolSizeUnit enum. -func (e AuthConfigResponseOutputDbMaxPoolSizeUnit) Valid() bool { - switch e { - case AuthConfigResponseOutputDbMaxPoolSizeUnitConnections: - return true - case AuthConfigResponseOutputDbMaxPoolSizeUnitLessThannil: - return true - case AuthConfigResponseOutputDbMaxPoolSizeUnitPercent: - return true - default: - return false - } -} - -// Defines values for AuthConfigResponseOutputPasswordRequiredCharacters. -const ( - AuthConfigResponseOutputPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 AuthConfigResponseOutputPasswordRequiredCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:0123456789" - AuthConfigResponseOutputPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567891 AuthConfigResponseOutputPasswordRequiredCharacters = "abcdefghijklmnopqrstuvwxyz:ABCDEFGHIJKLMNOPQRSTUVWXYZ:0123456789" - AuthConfigResponseOutputPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567892 AuthConfigResponseOutputPasswordRequiredCharacters = "abcdefghijklmnopqrstuvwxyz:ABCDEFGHIJKLMNOPQRSTUVWXYZ:0123456789:!@#$%^&*()_+-=[]{};'\\\\:\"|<>?,./`~" - AuthConfigResponseOutputPasswordRequiredCharactersEmpty AuthConfigResponseOutputPasswordRequiredCharacters = "" -) - -// Valid indicates whether the value is a known member of the AuthConfigResponseOutputPasswordRequiredCharacters enum. -func (e AuthConfigResponseOutputPasswordRequiredCharacters) Valid() bool { - switch e { - case AuthConfigResponseOutputPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789: - return true - case AuthConfigResponseOutputPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567891: - return true - case AuthConfigResponseOutputPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567892: - return true - case AuthConfigResponseOutputPasswordRequiredCharactersEmpty: - return true - default: - return false - } -} - -// Defines values for AuthConfigResponseOutputSecurityCaptchaProvider. -const ( - AuthConfigResponseOutputSecurityCaptchaProviderHcaptcha AuthConfigResponseOutputSecurityCaptchaProvider = "hcaptcha" - AuthConfigResponseOutputSecurityCaptchaProviderLessThannil AuthConfigResponseOutputSecurityCaptchaProvider = "" - AuthConfigResponseOutputSecurityCaptchaProviderTurnstile AuthConfigResponseOutputSecurityCaptchaProvider = "turnstile" -) - -// Valid indicates whether the value is a known member of the AuthConfigResponseOutputSecurityCaptchaProvider enum. -func (e AuthConfigResponseOutputSecurityCaptchaProvider) Valid() bool { - switch e { - case AuthConfigResponseOutputSecurityCaptchaProviderHcaptcha: - return true - case AuthConfigResponseOutputSecurityCaptchaProviderLessThannil: - return true - case AuthConfigResponseOutputSecurityCaptchaProviderTurnstile: - return true - default: - return false - } -} - -// Defines values for AuthConfigResponseOutputSmsProvider. -const ( - AuthConfigResponseOutputSmsProviderLessThannil AuthConfigResponseOutputSmsProvider = "" - AuthConfigResponseOutputSmsProviderMessagebird AuthConfigResponseOutputSmsProvider = "messagebird" - AuthConfigResponseOutputSmsProviderTextlocal AuthConfigResponseOutputSmsProvider = "textlocal" - AuthConfigResponseOutputSmsProviderTwilio AuthConfigResponseOutputSmsProvider = "twilio" - AuthConfigResponseOutputSmsProviderTwilioVerify AuthConfigResponseOutputSmsProvider = "twilio_verify" - AuthConfigResponseOutputSmsProviderVonage AuthConfigResponseOutputSmsProvider = "vonage" -) - -// Valid indicates whether the value is a known member of the AuthConfigResponseOutputSmsProvider enum. -func (e AuthConfigResponseOutputSmsProvider) Valid() bool { - switch e { - case AuthConfigResponseOutputSmsProviderLessThannil: - return true - case AuthConfigResponseOutputSmsProviderMessagebird: - return true - case AuthConfigResponseOutputSmsProviderTextlocal: - return true - case AuthConfigResponseOutputSmsProviderTwilio: - return true - case AuthConfigResponseOutputSmsProviderTwilioVerify: - return true - case AuthConfigResponseOutputSmsProviderVonage: - return true - default: - return false - } -} - -// Defines values for BranchDeleteResponseOutputMessage. -const ( - BranchDeleteResponseOutputMessageOk BranchDeleteResponseOutputMessage = "ok" -) - -// Valid indicates whether the value is a known member of the BranchDeleteResponseOutputMessage enum. -func (e BranchDeleteResponseOutputMessage) Valid() bool { - switch e { - case BranchDeleteResponseOutputMessageOk: - return true - default: - return false - } -} - -// Defines values for BranchDetailResponseOutputStatus. -const ( - BranchDetailResponseOutputStatusACTIVEHEALTHY BranchDetailResponseOutputStatus = "ACTIVE_HEALTHY" - BranchDetailResponseOutputStatusACTIVEUNHEALTHY BranchDetailResponseOutputStatus = "ACTIVE_UNHEALTHY" - BranchDetailResponseOutputStatusCOMINGUP BranchDetailResponseOutputStatus = "COMING_UP" - BranchDetailResponseOutputStatusGOINGDOWN BranchDetailResponseOutputStatus = "GOING_DOWN" - BranchDetailResponseOutputStatusINACTIVE BranchDetailResponseOutputStatus = "INACTIVE" - BranchDetailResponseOutputStatusINITFAILED BranchDetailResponseOutputStatus = "INIT_FAILED" - BranchDetailResponseOutputStatusPAUSEFAILED BranchDetailResponseOutputStatus = "PAUSE_FAILED" - BranchDetailResponseOutputStatusPAUSING BranchDetailResponseOutputStatus = "PAUSING" - BranchDetailResponseOutputStatusREMOVED BranchDetailResponseOutputStatus = "REMOVED" - BranchDetailResponseOutputStatusRESIZING BranchDetailResponseOutputStatus = "RESIZING" - BranchDetailResponseOutputStatusRESTARTING BranchDetailResponseOutputStatus = "RESTARTING" - BranchDetailResponseOutputStatusRESTOREFAILED BranchDetailResponseOutputStatus = "RESTORE_FAILED" - BranchDetailResponseOutputStatusRESTORING BranchDetailResponseOutputStatus = "RESTORING" - BranchDetailResponseOutputStatusUNKNOWN BranchDetailResponseOutputStatus = "UNKNOWN" - BranchDetailResponseOutputStatusUPGRADING BranchDetailResponseOutputStatus = "UPGRADING" -) - -// Valid indicates whether the value is a known member of the BranchDetailResponseOutputStatus enum. -func (e BranchDetailResponseOutputStatus) Valid() bool { - switch e { - case BranchDetailResponseOutputStatusACTIVEHEALTHY: - return true - case BranchDetailResponseOutputStatusACTIVEUNHEALTHY: - return true - case BranchDetailResponseOutputStatusCOMINGUP: - return true - case BranchDetailResponseOutputStatusGOINGDOWN: - return true - case BranchDetailResponseOutputStatusINACTIVE: - return true - case BranchDetailResponseOutputStatusINITFAILED: - return true - case BranchDetailResponseOutputStatusPAUSEFAILED: - return true - case BranchDetailResponseOutputStatusPAUSING: - return true - case BranchDetailResponseOutputStatusREMOVED: - return true - case BranchDetailResponseOutputStatusRESIZING: - return true - case BranchDetailResponseOutputStatusRESTARTING: - return true - case BranchDetailResponseOutputStatusRESTOREFAILED: - return true - case BranchDetailResponseOutputStatusRESTORING: - return true - case BranchDetailResponseOutputStatusUNKNOWN: - return true - case BranchDetailResponseOutputStatusUPGRADING: - return true - default: - return false - } -} - -// Defines values for BranchResponseOutputPreviewProjectStatus. -const ( - BranchResponseOutputPreviewProjectStatusACTIVEHEALTHY BranchResponseOutputPreviewProjectStatus = "ACTIVE_HEALTHY" - BranchResponseOutputPreviewProjectStatusACTIVEUNHEALTHY BranchResponseOutputPreviewProjectStatus = "ACTIVE_UNHEALTHY" - BranchResponseOutputPreviewProjectStatusCOMINGUP BranchResponseOutputPreviewProjectStatus = "COMING_UP" - BranchResponseOutputPreviewProjectStatusGOINGDOWN BranchResponseOutputPreviewProjectStatus = "GOING_DOWN" - BranchResponseOutputPreviewProjectStatusINACTIVE BranchResponseOutputPreviewProjectStatus = "INACTIVE" - BranchResponseOutputPreviewProjectStatusINITFAILED BranchResponseOutputPreviewProjectStatus = "INIT_FAILED" - BranchResponseOutputPreviewProjectStatusPAUSEFAILED BranchResponseOutputPreviewProjectStatus = "PAUSE_FAILED" - BranchResponseOutputPreviewProjectStatusPAUSING BranchResponseOutputPreviewProjectStatus = "PAUSING" - BranchResponseOutputPreviewProjectStatusREMOVED BranchResponseOutputPreviewProjectStatus = "REMOVED" - BranchResponseOutputPreviewProjectStatusRESIZING BranchResponseOutputPreviewProjectStatus = "RESIZING" - BranchResponseOutputPreviewProjectStatusRESTARTING BranchResponseOutputPreviewProjectStatus = "RESTARTING" - BranchResponseOutputPreviewProjectStatusRESTOREFAILED BranchResponseOutputPreviewProjectStatus = "RESTORE_FAILED" - BranchResponseOutputPreviewProjectStatusRESTORING BranchResponseOutputPreviewProjectStatus = "RESTORING" - BranchResponseOutputPreviewProjectStatusUNKNOWN BranchResponseOutputPreviewProjectStatus = "UNKNOWN" - BranchResponseOutputPreviewProjectStatusUPGRADING BranchResponseOutputPreviewProjectStatus = "UPGRADING" -) - -// Valid indicates whether the value is a known member of the BranchResponseOutputPreviewProjectStatus enum. -func (e BranchResponseOutputPreviewProjectStatus) Valid() bool { - switch e { - case BranchResponseOutputPreviewProjectStatusACTIVEHEALTHY: - return true - case BranchResponseOutputPreviewProjectStatusACTIVEUNHEALTHY: - return true - case BranchResponseOutputPreviewProjectStatusCOMINGUP: - return true - case BranchResponseOutputPreviewProjectStatusGOINGDOWN: - return true - case BranchResponseOutputPreviewProjectStatusINACTIVE: - return true - case BranchResponseOutputPreviewProjectStatusINITFAILED: - return true - case BranchResponseOutputPreviewProjectStatusPAUSEFAILED: - return true - case BranchResponseOutputPreviewProjectStatusPAUSING: - return true - case BranchResponseOutputPreviewProjectStatusREMOVED: - return true - case BranchResponseOutputPreviewProjectStatusRESIZING: - return true - case BranchResponseOutputPreviewProjectStatusRESTARTING: - return true - case BranchResponseOutputPreviewProjectStatusRESTOREFAILED: - return true - case BranchResponseOutputPreviewProjectStatusRESTORING: - return true - case BranchResponseOutputPreviewProjectStatusUNKNOWN: - return true - case BranchResponseOutputPreviewProjectStatusUPGRADING: - return true - default: - return false - } -} - -// Defines values for BranchResponseOutputStatus. -const ( - BranchResponseOutputStatusCREATINGPROJECT BranchResponseOutputStatus = "CREATING_PROJECT" - BranchResponseOutputStatusFUNCTIONSDEPLOYED BranchResponseOutputStatus = "FUNCTIONS_DEPLOYED" - BranchResponseOutputStatusFUNCTIONSFAILED BranchResponseOutputStatus = "FUNCTIONS_FAILED" - BranchResponseOutputStatusMIGRATIONSFAILED BranchResponseOutputStatus = "MIGRATIONS_FAILED" - BranchResponseOutputStatusMIGRATIONSPASSED BranchResponseOutputStatus = "MIGRATIONS_PASSED" - BranchResponseOutputStatusRUNNINGMIGRATIONS BranchResponseOutputStatus = "RUNNING_MIGRATIONS" -) - -// Valid indicates whether the value is a known member of the BranchResponseOutputStatus enum. -func (e BranchResponseOutputStatus) Valid() bool { - switch e { - case BranchResponseOutputStatusCREATINGPROJECT: - return true - case BranchResponseOutputStatusFUNCTIONSDEPLOYED: - return true - case BranchResponseOutputStatusFUNCTIONSFAILED: - return true - case BranchResponseOutputStatusMIGRATIONSFAILED: - return true - case BranchResponseOutputStatusMIGRATIONSPASSED: - return true - case BranchResponseOutputStatusRUNNINGMIGRATIONS: - return true - default: - return false - } -} - -// Defines values for BranchRestoreResponseOutputMessage. -const ( - BranchRestorationInitiated BranchRestoreResponseOutputMessage = "Branch restoration initiated" -) - -// Valid indicates whether the value is a known member of the BranchRestoreResponseOutputMessage enum. -func (e BranchRestoreResponseOutputMessage) Valid() bool { - switch e { - case BranchRestorationInitiated: - return true - default: - return false - } -} - -// Defines values for BranchUpdateResponseOutputMessage. -const ( - BranchUpdateResponseOutputMessageOk BranchUpdateResponseOutputMessage = "ok" -) - -// Valid indicates whether the value is a known member of the BranchUpdateResponseOutputMessage enum. -func (e BranchUpdateResponseOutputMessage) Valid() bool { - switch e { - case BranchUpdateResponseOutputMessageOk: - return true - default: - return false - } -} - -// Defines values for BulkUpdateFunctionBodyStatus. -const ( - BulkUpdateFunctionBodyStatusACTIVE BulkUpdateFunctionBodyStatus = "ACTIVE" - BulkUpdateFunctionBodyStatusREMOVED BulkUpdateFunctionBodyStatus = "REMOVED" - BulkUpdateFunctionBodyStatusTHROTTLED BulkUpdateFunctionBodyStatus = "THROTTLED" -) - -// Valid indicates whether the value is a known member of the BulkUpdateFunctionBodyStatus enum. -func (e BulkUpdateFunctionBodyStatus) Valid() bool { - switch e { - case BulkUpdateFunctionBodyStatusACTIVE: - return true - case BulkUpdateFunctionBodyStatusREMOVED: - return true - case BulkUpdateFunctionBodyStatusTHROTTLED: - return true - default: - return false - } -} - -// Defines values for BulkUpdateFunctionResponseOutputFunctionsStatus. -const ( - BulkUpdateFunctionResponseOutputFunctionsStatusACTIVE BulkUpdateFunctionResponseOutputFunctionsStatus = "ACTIVE" - BulkUpdateFunctionResponseOutputFunctionsStatusREMOVED BulkUpdateFunctionResponseOutputFunctionsStatus = "REMOVED" - BulkUpdateFunctionResponseOutputFunctionsStatusTHROTTLED BulkUpdateFunctionResponseOutputFunctionsStatus = "THROTTLED" -) - -// Valid indicates whether the value is a known member of the BulkUpdateFunctionResponseOutputFunctionsStatus enum. -func (e BulkUpdateFunctionResponseOutputFunctionsStatus) Valid() bool { - switch e { - case BulkUpdateFunctionResponseOutputFunctionsStatusACTIVE: - return true - case BulkUpdateFunctionResponseOutputFunctionsStatusREMOVED: - return true - case BulkUpdateFunctionResponseOutputFunctionsStatusTHROTTLED: - return true - default: - return false - } -} - -// Defines values for CreateApiKeyBodyType. -const ( - Publishable CreateApiKeyBodyType = "publishable" - Secret CreateApiKeyBodyType = "secret" -) - -// Valid indicates whether the value is a known member of the CreateApiKeyBodyType enum. -func (e CreateApiKeyBodyType) Valid() bool { - switch e { - case Publishable: - return true - case Secret: - return true - default: - return false - } -} - -// Defines values for CreateBranchBodyDesiredInstanceSize. -const ( - CreateBranchBodyDesiredInstanceSizeLarge CreateBranchBodyDesiredInstanceSize = "large" - CreateBranchBodyDesiredInstanceSizeMedium CreateBranchBodyDesiredInstanceSize = "medium" - CreateBranchBodyDesiredInstanceSizeMicro CreateBranchBodyDesiredInstanceSize = "micro" - CreateBranchBodyDesiredInstanceSizeN12xlarge CreateBranchBodyDesiredInstanceSize = "12xlarge" - CreateBranchBodyDesiredInstanceSizeN16xlarge CreateBranchBodyDesiredInstanceSize = "16xlarge" - CreateBranchBodyDesiredInstanceSizeN24xlarge CreateBranchBodyDesiredInstanceSize = "24xlarge" - CreateBranchBodyDesiredInstanceSizeN24xlargeHighMemory CreateBranchBodyDesiredInstanceSize = "24xlarge_high_memory" - CreateBranchBodyDesiredInstanceSizeN24xlargeOptimizedCpu CreateBranchBodyDesiredInstanceSize = "24xlarge_optimized_cpu" - CreateBranchBodyDesiredInstanceSizeN24xlargeOptimizedMemory CreateBranchBodyDesiredInstanceSize = "24xlarge_optimized_memory" - CreateBranchBodyDesiredInstanceSizeN2xlarge CreateBranchBodyDesiredInstanceSize = "2xlarge" - CreateBranchBodyDesiredInstanceSizeN48xlarge CreateBranchBodyDesiredInstanceSize = "48xlarge" - CreateBranchBodyDesiredInstanceSizeN48xlargeHighMemory CreateBranchBodyDesiredInstanceSize = "48xlarge_high_memory" - CreateBranchBodyDesiredInstanceSizeN48xlargeOptimizedCpu CreateBranchBodyDesiredInstanceSize = "48xlarge_optimized_cpu" - CreateBranchBodyDesiredInstanceSizeN48xlargeOptimizedMemory CreateBranchBodyDesiredInstanceSize = "48xlarge_optimized_memory" - CreateBranchBodyDesiredInstanceSizeN4xlarge CreateBranchBodyDesiredInstanceSize = "4xlarge" - CreateBranchBodyDesiredInstanceSizeN8xlarge CreateBranchBodyDesiredInstanceSize = "8xlarge" - CreateBranchBodyDesiredInstanceSizeNano CreateBranchBodyDesiredInstanceSize = "nano" - CreateBranchBodyDesiredInstanceSizePico CreateBranchBodyDesiredInstanceSize = "pico" - CreateBranchBodyDesiredInstanceSizeSmall CreateBranchBodyDesiredInstanceSize = "small" - CreateBranchBodyDesiredInstanceSizeXlarge CreateBranchBodyDesiredInstanceSize = "xlarge" -) - -// Valid indicates whether the value is a known member of the CreateBranchBodyDesiredInstanceSize enum. -func (e CreateBranchBodyDesiredInstanceSize) Valid() bool { - switch e { - case CreateBranchBodyDesiredInstanceSizeLarge: - return true - case CreateBranchBodyDesiredInstanceSizeMedium: - return true - case CreateBranchBodyDesiredInstanceSizeMicro: - return true - case CreateBranchBodyDesiredInstanceSizeN12xlarge: - return true - case CreateBranchBodyDesiredInstanceSizeN16xlarge: - return true - case CreateBranchBodyDesiredInstanceSizeN24xlarge: - return true - case CreateBranchBodyDesiredInstanceSizeN24xlargeHighMemory: - return true - case CreateBranchBodyDesiredInstanceSizeN24xlargeOptimizedCpu: - return true - case CreateBranchBodyDesiredInstanceSizeN24xlargeOptimizedMemory: - return true - case CreateBranchBodyDesiredInstanceSizeN2xlarge: - return true - case CreateBranchBodyDesiredInstanceSizeN48xlarge: - return true - case CreateBranchBodyDesiredInstanceSizeN48xlargeHighMemory: - return true - case CreateBranchBodyDesiredInstanceSizeN48xlargeOptimizedCpu: - return true - case CreateBranchBodyDesiredInstanceSizeN48xlargeOptimizedMemory: - return true - case CreateBranchBodyDesiredInstanceSizeN4xlarge: - return true - case CreateBranchBodyDesiredInstanceSizeN8xlarge: - return true - case CreateBranchBodyDesiredInstanceSizeNano: - return true - case CreateBranchBodyDesiredInstanceSizePico: - return true - case CreateBranchBodyDesiredInstanceSizeSmall: - return true - case CreateBranchBodyDesiredInstanceSizeXlarge: - return true - default: - return false - } -} - -// Defines values for CreateBranchBodyPostgresEngine. -const ( - CreateBranchBodyPostgresEngineN15 CreateBranchBodyPostgresEngine = "15" - CreateBranchBodyPostgresEngineN17 CreateBranchBodyPostgresEngine = "17" - CreateBranchBodyPostgresEngineN17Oriole CreateBranchBodyPostgresEngine = "17-oriole" -) - -// Valid indicates whether the value is a known member of the CreateBranchBodyPostgresEngine enum. -func (e CreateBranchBodyPostgresEngine) Valid() bool { - switch e { - case CreateBranchBodyPostgresEngineN15: - return true - case CreateBranchBodyPostgresEngineN17: - return true - case CreateBranchBodyPostgresEngineN17Oriole: - return true - default: - return false - } -} - -// Defines values for CreateBranchBodyReleaseChannel. -const ( - CreateBranchBodyReleaseChannelAlpha CreateBranchBodyReleaseChannel = "alpha" - CreateBranchBodyReleaseChannelBeta CreateBranchBodyReleaseChannel = "beta" - CreateBranchBodyReleaseChannelGa CreateBranchBodyReleaseChannel = "ga" - CreateBranchBodyReleaseChannelInternal CreateBranchBodyReleaseChannel = "internal" - CreateBranchBodyReleaseChannelPreview CreateBranchBodyReleaseChannel = "preview" - CreateBranchBodyReleaseChannelWithdrawn CreateBranchBodyReleaseChannel = "withdrawn" -) - -// Valid indicates whether the value is a known member of the CreateBranchBodyReleaseChannel enum. -func (e CreateBranchBodyReleaseChannel) Valid() bool { - switch e { - case CreateBranchBodyReleaseChannelAlpha: - return true - case CreateBranchBodyReleaseChannelBeta: - return true - case CreateBranchBodyReleaseChannelGa: - return true - case CreateBranchBodyReleaseChannelInternal: - return true - case CreateBranchBodyReleaseChannelPreview: - return true - case CreateBranchBodyReleaseChannelWithdrawn: - return true - default: - return false - } -} - -// Defines values for CreateProviderBodyNameIdFormat. -const ( - CreateProviderBodyNameIdFormatUrnOasisNamesTcSAML11NameidFormatEmailAddress CreateProviderBodyNameIdFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" - CreateProviderBodyNameIdFormatUrnOasisNamesTcSAML11NameidFormatUnspecified CreateProviderBodyNameIdFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" - CreateProviderBodyNameIdFormatUrnOasisNamesTcSAML20NameidFormatPersistent CreateProviderBodyNameIdFormat = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" - CreateProviderBodyNameIdFormatUrnOasisNamesTcSAML20NameidFormatTransient CreateProviderBodyNameIdFormat = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" -) - -// Valid indicates whether the value is a known member of the CreateProviderBodyNameIdFormat enum. -func (e CreateProviderBodyNameIdFormat) Valid() bool { - switch e { - case CreateProviderBodyNameIdFormatUrnOasisNamesTcSAML11NameidFormatEmailAddress: - return true - case CreateProviderBodyNameIdFormatUrnOasisNamesTcSAML11NameidFormatUnspecified: - return true - case CreateProviderBodyNameIdFormatUrnOasisNamesTcSAML20NameidFormatPersistent: - return true - case CreateProviderBodyNameIdFormatUrnOasisNamesTcSAML20NameidFormatTransient: - return true - default: - return false - } -} - -// Defines values for CreateProviderBodyType. -const ( - Saml CreateProviderBodyType = "saml" -) - -// Valid indicates whether the value is a known member of the CreateProviderBodyType enum. -func (e CreateProviderBodyType) Valid() bool { - switch e { - case Saml: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyAlgorithm. -const ( - CreateSigningKeyBodyAlgorithmES256 CreateSigningKeyBodyAlgorithm = "ES256" - CreateSigningKeyBodyAlgorithmEdDSA CreateSigningKeyBodyAlgorithm = "EdDSA" - CreateSigningKeyBodyAlgorithmHS256 CreateSigningKeyBodyAlgorithm = "HS256" - CreateSigningKeyBodyAlgorithmRS256 CreateSigningKeyBodyAlgorithm = "RS256" -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyAlgorithm enum. -func (e CreateSigningKeyBodyAlgorithm) Valid() bool { - switch e { - case CreateSigningKeyBodyAlgorithmES256: - return true - case CreateSigningKeyBodyAlgorithmEdDSA: - return true - case CreateSigningKeyBodyAlgorithmHS256: - return true - case CreateSigningKeyBodyAlgorithmRS256: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk0Alg. -const ( - CreateSigningKeyBodyPrivateJwk0AlgRS256 CreateSigningKeyBodyPrivateJwk0Alg = "RS256" -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk0Alg enum. -func (e CreateSigningKeyBodyPrivateJwk0Alg) Valid() bool { - switch e { - case CreateSigningKeyBodyPrivateJwk0AlgRS256: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk0E. -const ( - AQAB CreateSigningKeyBodyPrivateJwk0E = "AQAB" -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk0E enum. -func (e CreateSigningKeyBodyPrivateJwk0E) Valid() bool { - switch e { - case AQAB: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk0Ext. -const ( - CreateSigningKeyBodyPrivateJwk0ExtTrue CreateSigningKeyBodyPrivateJwk0Ext = true -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk0Ext enum. -func (e CreateSigningKeyBodyPrivateJwk0Ext) Valid() bool { - switch e { - case CreateSigningKeyBodyPrivateJwk0ExtTrue: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk0KeyOps. -const ( - CreateSigningKeyBodyPrivateJwk0KeyOpsSign CreateSigningKeyBodyPrivateJwk0KeyOps = "sign" - CreateSigningKeyBodyPrivateJwk0KeyOpsVerify CreateSigningKeyBodyPrivateJwk0KeyOps = "verify" -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk0KeyOps enum. -func (e CreateSigningKeyBodyPrivateJwk0KeyOps) Valid() bool { - switch e { - case CreateSigningKeyBodyPrivateJwk0KeyOpsSign: - return true - case CreateSigningKeyBodyPrivateJwk0KeyOpsVerify: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk0Kty. -const ( - RSA CreateSigningKeyBodyPrivateJwk0Kty = "RSA" -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk0Kty enum. -func (e CreateSigningKeyBodyPrivateJwk0Kty) Valid() bool { - switch e { - case RSA: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk0Use. -const ( - CreateSigningKeyBodyPrivateJwk0UseSig CreateSigningKeyBodyPrivateJwk0Use = "sig" -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk0Use enum. -func (e CreateSigningKeyBodyPrivateJwk0Use) Valid() bool { - switch e { - case CreateSigningKeyBodyPrivateJwk0UseSig: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk1Alg. -const ( - CreateSigningKeyBodyPrivateJwk1AlgES256 CreateSigningKeyBodyPrivateJwk1Alg = "ES256" -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk1Alg enum. -func (e CreateSigningKeyBodyPrivateJwk1Alg) Valid() bool { - switch e { - case CreateSigningKeyBodyPrivateJwk1AlgES256: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk1Crv. -const ( - P256 CreateSigningKeyBodyPrivateJwk1Crv = "P-256" -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk1Crv enum. -func (e CreateSigningKeyBodyPrivateJwk1Crv) Valid() bool { - switch e { - case P256: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk1Ext. -const ( - CreateSigningKeyBodyPrivateJwk1ExtTrue CreateSigningKeyBodyPrivateJwk1Ext = true -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk1Ext enum. -func (e CreateSigningKeyBodyPrivateJwk1Ext) Valid() bool { - switch e { - case CreateSigningKeyBodyPrivateJwk1ExtTrue: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk1KeyOps. -const ( - CreateSigningKeyBodyPrivateJwk1KeyOpsSign CreateSigningKeyBodyPrivateJwk1KeyOps = "sign" - CreateSigningKeyBodyPrivateJwk1KeyOpsVerify CreateSigningKeyBodyPrivateJwk1KeyOps = "verify" -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk1KeyOps enum. -func (e CreateSigningKeyBodyPrivateJwk1KeyOps) Valid() bool { - switch e { - case CreateSigningKeyBodyPrivateJwk1KeyOpsSign: - return true - case CreateSigningKeyBodyPrivateJwk1KeyOpsVerify: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk1Kty. -const ( - EC CreateSigningKeyBodyPrivateJwk1Kty = "EC" -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk1Kty enum. -func (e CreateSigningKeyBodyPrivateJwk1Kty) Valid() bool { - switch e { - case EC: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk1Use. -const ( - CreateSigningKeyBodyPrivateJwk1UseSig CreateSigningKeyBodyPrivateJwk1Use = "sig" -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk1Use enum. -func (e CreateSigningKeyBodyPrivateJwk1Use) Valid() bool { - switch e { - case CreateSigningKeyBodyPrivateJwk1UseSig: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk2Alg. -const ( - CreateSigningKeyBodyPrivateJwk2AlgEdDSA CreateSigningKeyBodyPrivateJwk2Alg = "EdDSA" -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk2Alg enum. -func (e CreateSigningKeyBodyPrivateJwk2Alg) Valid() bool { - switch e { - case CreateSigningKeyBodyPrivateJwk2AlgEdDSA: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk2Crv. -const ( - Ed25519 CreateSigningKeyBodyPrivateJwk2Crv = "Ed25519" -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk2Crv enum. -func (e CreateSigningKeyBodyPrivateJwk2Crv) Valid() bool { - switch e { - case Ed25519: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk2Ext. -const ( - CreateSigningKeyBodyPrivateJwk2ExtTrue CreateSigningKeyBodyPrivateJwk2Ext = true -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk2Ext enum. -func (e CreateSigningKeyBodyPrivateJwk2Ext) Valid() bool { - switch e { - case CreateSigningKeyBodyPrivateJwk2ExtTrue: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk2KeyOps. -const ( - CreateSigningKeyBodyPrivateJwk2KeyOpsSign CreateSigningKeyBodyPrivateJwk2KeyOps = "sign" - CreateSigningKeyBodyPrivateJwk2KeyOpsVerify CreateSigningKeyBodyPrivateJwk2KeyOps = "verify" -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk2KeyOps enum. -func (e CreateSigningKeyBodyPrivateJwk2KeyOps) Valid() bool { - switch e { - case CreateSigningKeyBodyPrivateJwk2KeyOpsSign: - return true - case CreateSigningKeyBodyPrivateJwk2KeyOpsVerify: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk2Kty. -const ( - OKP CreateSigningKeyBodyPrivateJwk2Kty = "OKP" -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk2Kty enum. -func (e CreateSigningKeyBodyPrivateJwk2Kty) Valid() bool { - switch e { - case OKP: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk2Use. -const ( - CreateSigningKeyBodyPrivateJwk2UseSig CreateSigningKeyBodyPrivateJwk2Use = "sig" -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk2Use enum. -func (e CreateSigningKeyBodyPrivateJwk2Use) Valid() bool { - switch e { - case CreateSigningKeyBodyPrivateJwk2UseSig: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk3Alg. -const ( - HS256 CreateSigningKeyBodyPrivateJwk3Alg = "HS256" -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk3Alg enum. -func (e CreateSigningKeyBodyPrivateJwk3Alg) Valid() bool { - switch e { - case HS256: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk3Ext. -const ( - CreateSigningKeyBodyPrivateJwk3ExtTrue CreateSigningKeyBodyPrivateJwk3Ext = true -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk3Ext enum. -func (e CreateSigningKeyBodyPrivateJwk3Ext) Valid() bool { - switch e { - case CreateSigningKeyBodyPrivateJwk3ExtTrue: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk3KeyOps. -const ( - CreateSigningKeyBodyPrivateJwk3KeyOpsSign CreateSigningKeyBodyPrivateJwk3KeyOps = "sign" - CreateSigningKeyBodyPrivateJwk3KeyOpsVerify CreateSigningKeyBodyPrivateJwk3KeyOps = "verify" -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk3KeyOps enum. -func (e CreateSigningKeyBodyPrivateJwk3KeyOps) Valid() bool { - switch e { - case CreateSigningKeyBodyPrivateJwk3KeyOpsSign: - return true - case CreateSigningKeyBodyPrivateJwk3KeyOpsVerify: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk3Kty. -const ( - Oct CreateSigningKeyBodyPrivateJwk3Kty = "oct" -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk3Kty enum. -func (e CreateSigningKeyBodyPrivateJwk3Kty) Valid() bool { - switch e { - case Oct: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyPrivateJwk3Use. -const ( - CreateSigningKeyBodyPrivateJwk3UseSig CreateSigningKeyBodyPrivateJwk3Use = "sig" -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk3Use enum. -func (e CreateSigningKeyBodyPrivateJwk3Use) Valid() bool { - switch e { - case CreateSigningKeyBodyPrivateJwk3UseSig: - return true - default: - return false - } -} - -// Defines values for CreateSigningKeyBodyStatus. -const ( - CreateSigningKeyBodyStatusInUse CreateSigningKeyBodyStatus = "in_use" - CreateSigningKeyBodyStatusStandby CreateSigningKeyBodyStatus = "standby" -) - -// Valid indicates whether the value is a known member of the CreateSigningKeyBodyStatus enum. -func (e CreateSigningKeyBodyStatus) Valid() bool { - switch e { - case CreateSigningKeyBodyStatusInUse: - return true - case CreateSigningKeyBodyStatusStandby: - return true - default: - return false - } -} - -// Defines values for DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusError. -const ( - N1UpgradedInstanceLaunchFailed DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusError = "1_upgraded_instance_launch_failed" - N2VolumeDetachchmentFromUpgradedInstanceFailed DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusError = "2_volume_detachchment_from_upgraded_instance_failed" - N3VolumeAttachmentToOriginalInstanceFailed DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusError = "3_volume_attachment_to_original_instance_failed" - N4DataUpgradeInitiationFailed DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusError = "4_data_upgrade_initiation_failed" - N5DataUpgradeCompletionFailed DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusError = "5_data_upgrade_completion_failed" - N6VolumeDetachchmentFromOriginalInstanceFailed DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusError = "6_volume_detachchment_from_original_instance_failed" - N7VolumeAttachmentToUpgradedInstanceFailed DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusError = "7_volume_attachment_to_upgraded_instance_failed" - N8UpgradeCompletionFailed DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusError = "8_upgrade_completion_failed" - N9PostPhysicalBackupFailed DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusError = "9_post_physical_backup_failed" -) - -// Valid indicates whether the value is a known member of the DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusError enum. -func (e DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusError) Valid() bool { - switch e { - case N1UpgradedInstanceLaunchFailed: - return true - case N2VolumeDetachchmentFromUpgradedInstanceFailed: - return true - case N3VolumeAttachmentToOriginalInstanceFailed: - return true - case N4DataUpgradeInitiationFailed: - return true - case N5DataUpgradeCompletionFailed: - return true - case N6VolumeDetachchmentFromOriginalInstanceFailed: - return true - case N7VolumeAttachmentToUpgradedInstanceFailed: - return true - case N8UpgradeCompletionFailed: - return true - case N9PostPhysicalBackupFailed: - return true - default: - return false - } -} - -// Defines values for DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusProgress. -const ( - N0Requested DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusProgress = "0_requested" - N10CompletedPostPhysicalBackup DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusProgress = "10_completed_post_physical_backup" - N1Started DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusProgress = "1_started" - N2LaunchedUpgradedInstance DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusProgress = "2_launched_upgraded_instance" - N3DetachedVolumeFromUpgradedInstance DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusProgress = "3_detached_volume_from_upgraded_instance" - N4AttachedVolumeToOriginalInstance DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusProgress = "4_attached_volume_to_original_instance" - N5InitiatedDataUpgrade DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusProgress = "5_initiated_data_upgrade" - N6CompletedDataUpgrade DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusProgress = "6_completed_data_upgrade" - N7DetachedVolumeFromOriginalInstance DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusProgress = "7_detached_volume_from_original_instance" - N8AttachedVolumeToUpgradedInstance DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusProgress = "8_attached_volume_to_upgraded_instance" - N9CompletedUpgrade DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusProgress = "9_completed_upgrade" -) - -// Valid indicates whether the value is a known member of the DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusProgress enum. -func (e DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusProgress) Valid() bool { - switch e { - case N0Requested: - return true - case N10CompletedPostPhysicalBackup: - return true - case N1Started: - return true - case N2LaunchedUpgradedInstance: - return true - case N3DetachedVolumeFromUpgradedInstance: - return true - case N4AttachedVolumeToOriginalInstance: - return true - case N5InitiatedDataUpgrade: - return true - case N6CompletedDataUpgrade: - return true - case N7DetachedVolumeFromOriginalInstance: - return true - case N8AttachedVolumeToUpgradedInstance: - return true - case N9CompletedUpgrade: - return true - default: - return false - } -} - -// Defines values for DeleteRolesResponseOutputMessage. -const ( - DeleteRolesResponseOutputMessageOk DeleteRolesResponseOutputMessage = "ok" -) - -// Valid indicates whether the value is a known member of the DeleteRolesResponseOutputMessage enum. -func (e DeleteRolesResponseOutputMessage) Valid() bool { - switch e { - case DeleteRolesResponseOutputMessageOk: - return true - default: - return false - } -} - -// Defines values for DeployFunctionResponseOutputStatus. -const ( - DeployFunctionResponseOutputStatusACTIVE DeployFunctionResponseOutputStatus = "ACTIVE" - DeployFunctionResponseOutputStatusREMOVED DeployFunctionResponseOutputStatus = "REMOVED" - DeployFunctionResponseOutputStatusTHROTTLED DeployFunctionResponseOutputStatus = "THROTTLED" -) - -// Valid indicates whether the value is a known member of the DeployFunctionResponseOutputStatus enum. -func (e DeployFunctionResponseOutputStatus) Valid() bool { - switch e { - case DeployFunctionResponseOutputStatusACTIVE: - return true - case DeployFunctionResponseOutputStatusREMOVED: - return true - case DeployFunctionResponseOutputStatusTHROTTLED: - return true - default: - return false - } -} - -// Defines values for DiskRequestBodyAttributes0Type. -const ( - DiskRequestBodyAttributes0TypeGp3 DiskRequestBodyAttributes0Type = "gp3" -) - -// Valid indicates whether the value is a known member of the DiskRequestBodyAttributes0Type enum. -func (e DiskRequestBodyAttributes0Type) Valid() bool { - switch e { - case DiskRequestBodyAttributes0TypeGp3: - return true - default: - return false - } -} - -// Defines values for DiskRequestBodyAttributes1Type. -const ( - DiskRequestBodyAttributes1TypeIo2 DiskRequestBodyAttributes1Type = "io2" -) - -// Valid indicates whether the value is a known member of the DiskRequestBodyAttributes1Type enum. -func (e DiskRequestBodyAttributes1Type) Valid() bool { - switch e { - case DiskRequestBodyAttributes1TypeIo2: - return true - default: - return false - } -} - -// Defines values for DiskResponseOutputAttributes0Type. -const ( - DiskResponseOutputAttributes0TypeGp3 DiskResponseOutputAttributes0Type = "gp3" -) - -// Valid indicates whether the value is a known member of the DiskResponseOutputAttributes0Type enum. -func (e DiskResponseOutputAttributes0Type) Valid() bool { - switch e { - case DiskResponseOutputAttributes0TypeGp3: - return true - default: - return false - } -} - -// Defines values for DiskResponseOutputAttributes1Type. -const ( - DiskResponseOutputAttributes1TypeIo2 DiskResponseOutputAttributes1Type = "io2" -) - -// Valid indicates whether the value is a known member of the DiskResponseOutputAttributes1Type enum. -func (e DiskResponseOutputAttributes1Type) Valid() bool { - switch e { - case DiskResponseOutputAttributes1TypeIo2: - return true - default: - return false - } -} - -// Defines values for FunctionResponseOutputStatus. -const ( - FunctionResponseOutputStatusACTIVE FunctionResponseOutputStatus = "ACTIVE" - FunctionResponseOutputStatusREMOVED FunctionResponseOutputStatus = "REMOVED" - FunctionResponseOutputStatusTHROTTLED FunctionResponseOutputStatus = "THROTTLED" -) - -// Valid indicates whether the value is a known member of the FunctionResponseOutputStatus enum. -func (e FunctionResponseOutputStatus) Valid() bool { - switch e { - case FunctionResponseOutputStatusACTIVE: - return true - case FunctionResponseOutputStatusREMOVED: - return true - case FunctionResponseOutputStatusTHROTTLED: - return true - default: - return false - } -} - -// Defines values for FunctionSlugResponseOutputStatus. -const ( - FunctionSlugResponseOutputStatusACTIVE FunctionSlugResponseOutputStatus = "ACTIVE" - FunctionSlugResponseOutputStatusREMOVED FunctionSlugResponseOutputStatus = "REMOVED" - FunctionSlugResponseOutputStatusTHROTTLED FunctionSlugResponseOutputStatus = "THROTTLED" -) - -// Valid indicates whether the value is a known member of the FunctionSlugResponseOutputStatus enum. -func (e FunctionSlugResponseOutputStatus) Valid() bool { - switch e { - case FunctionSlugResponseOutputStatusACTIVE: - return true - case FunctionSlugResponseOutputStatusREMOVED: - return true - case FunctionSlugResponseOutputStatusTHROTTLED: - return true - default: - return false - } -} - -// Defines values for GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsPostgresEngine. -const ( - GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsPostgresEngineN13 GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsPostgresEngine = "13" - GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsPostgresEngineN14 GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsPostgresEngine = "14" - GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsPostgresEngineN15 GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsPostgresEngine = "15" - GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsPostgresEngineN17 GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsPostgresEngine = "17" - GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsPostgresEngineN17Oriole GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsPostgresEngine = "17-oriole" -) - -// Valid indicates whether the value is a known member of the GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsPostgresEngine enum. -func (e GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsPostgresEngine) Valid() bool { - switch e { - case GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsPostgresEngineN13: - return true - case GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsPostgresEngineN14: - return true - case GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsPostgresEngineN15: - return true - case GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsPostgresEngineN17: - return true - case GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsPostgresEngineN17Oriole: - return true - default: - return false - } -} - -// Defines values for GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannel. -const ( - GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannelAlpha GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannel = "alpha" - GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannelBeta GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannel = "beta" - GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannelGa GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannel = "ga" - GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannelInternal GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannel = "internal" - GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannelPreview GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannel = "preview" - GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannelWithdrawn GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannel = "withdrawn" -) - -// Valid indicates whether the value is a known member of the GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannel enum. -func (e GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannel) Valid() bool { - switch e { - case GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannelAlpha: - return true - case GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannelBeta: - return true - case GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannelGa: - return true - case GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannelInternal: - return true - case GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannelPreview: - return true - case GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannelWithdrawn: - return true - default: - return false - } -} - -// Defines values for JitAccessRequestRequestState. -const ( - JitAccessRequestRequestStateDisabled JitAccessRequestRequestState = "disabled" - JitAccessRequestRequestStateEnabled JitAccessRequestRequestState = "enabled" -) - -// Valid indicates whether the value is a known member of the JitAccessRequestRequestState enum. -func (e JitAccessRequestRequestState) Valid() bool { - switch e { - case JitAccessRequestRequestStateDisabled: - return true - case JitAccessRequestRequestStateEnabled: - return true - default: - return false - } -} - -// Defines values for ListActionRunResponseOutputRunStepsName. -const ( - ListActionRunResponseOutputRunStepsNameClone ListActionRunResponseOutputRunStepsName = "clone" - ListActionRunResponseOutputRunStepsNameConfigure ListActionRunResponseOutputRunStepsName = "configure" - ListActionRunResponseOutputRunStepsNameDeploy ListActionRunResponseOutputRunStepsName = "deploy" - ListActionRunResponseOutputRunStepsNameHealth ListActionRunResponseOutputRunStepsName = "health" - ListActionRunResponseOutputRunStepsNameMigrate ListActionRunResponseOutputRunStepsName = "migrate" - ListActionRunResponseOutputRunStepsNamePull ListActionRunResponseOutputRunStepsName = "pull" - ListActionRunResponseOutputRunStepsNameSeed ListActionRunResponseOutputRunStepsName = "seed" -) - -// Valid indicates whether the value is a known member of the ListActionRunResponseOutputRunStepsName enum. -func (e ListActionRunResponseOutputRunStepsName) Valid() bool { - switch e { - case ListActionRunResponseOutputRunStepsNameClone: - return true - case ListActionRunResponseOutputRunStepsNameConfigure: - return true - case ListActionRunResponseOutputRunStepsNameDeploy: - return true - case ListActionRunResponseOutputRunStepsNameHealth: - return true - case ListActionRunResponseOutputRunStepsNameMigrate: - return true - case ListActionRunResponseOutputRunStepsNamePull: - return true - case ListActionRunResponseOutputRunStepsNameSeed: - return true - default: - return false - } -} - -// Defines values for ListActionRunResponseOutputRunStepsStatus. -const ( - ListActionRunResponseOutputRunStepsStatusCREATED ListActionRunResponseOutputRunStepsStatus = "CREATED" - ListActionRunResponseOutputRunStepsStatusDEAD ListActionRunResponseOutputRunStepsStatus = "DEAD" - ListActionRunResponseOutputRunStepsStatusEXITED ListActionRunResponseOutputRunStepsStatus = "EXITED" - ListActionRunResponseOutputRunStepsStatusPAUSED ListActionRunResponseOutputRunStepsStatus = "PAUSED" - ListActionRunResponseOutputRunStepsStatusREMOVING ListActionRunResponseOutputRunStepsStatus = "REMOVING" - ListActionRunResponseOutputRunStepsStatusRESTARTING ListActionRunResponseOutputRunStepsStatus = "RESTARTING" - ListActionRunResponseOutputRunStepsStatusRUNNING ListActionRunResponseOutputRunStepsStatus = "RUNNING" -) - -// Valid indicates whether the value is a known member of the ListActionRunResponseOutputRunStepsStatus enum. -func (e ListActionRunResponseOutputRunStepsStatus) Valid() bool { - switch e { - case ListActionRunResponseOutputRunStepsStatusCREATED: - return true - case ListActionRunResponseOutputRunStepsStatusDEAD: - return true - case ListActionRunResponseOutputRunStepsStatusEXITED: - return true - case ListActionRunResponseOutputRunStepsStatusPAUSED: - return true - case ListActionRunResponseOutputRunStepsStatusREMOVING: - return true - case ListActionRunResponseOutputRunStepsStatusRESTARTING: - return true - case ListActionRunResponseOutputRunStepsStatusRUNNING: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputAvailableAddonsType. -const ( - ListProjectAddonsResponseOutputAvailableAddonsTypeAuthMfaPhone ListProjectAddonsResponseOutputAvailableAddonsType = "auth_mfa_phone" - ListProjectAddonsResponseOutputAvailableAddonsTypeAuthMfaWebAuthn ListProjectAddonsResponseOutputAvailableAddonsType = "auth_mfa_web_authn" - ListProjectAddonsResponseOutputAvailableAddonsTypeComputeInstance ListProjectAddonsResponseOutputAvailableAddonsType = "compute_instance" - ListProjectAddonsResponseOutputAvailableAddonsTypeCustomDomain ListProjectAddonsResponseOutputAvailableAddonsType = "custom_domain" - ListProjectAddonsResponseOutputAvailableAddonsTypeEtlPipeline ListProjectAddonsResponseOutputAvailableAddonsType = "etl_pipeline" - ListProjectAddonsResponseOutputAvailableAddonsTypeIpv4 ListProjectAddonsResponseOutputAvailableAddonsType = "ipv4" - ListProjectAddonsResponseOutputAvailableAddonsTypeLogDrain ListProjectAddonsResponseOutputAvailableAddonsType = "log_drain" - ListProjectAddonsResponseOutputAvailableAddonsTypePitr ListProjectAddonsResponseOutputAvailableAddonsType = "pitr" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputAvailableAddonsType enum. -func (e ListProjectAddonsResponseOutputAvailableAddonsType) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputAvailableAddonsTypeAuthMfaPhone: - return true - case ListProjectAddonsResponseOutputAvailableAddonsTypeAuthMfaWebAuthn: - return true - case ListProjectAddonsResponseOutputAvailableAddonsTypeComputeInstance: - return true - case ListProjectAddonsResponseOutputAvailableAddonsTypeCustomDomain: - return true - case ListProjectAddonsResponseOutputAvailableAddonsTypeEtlPipeline: - return true - case ListProjectAddonsResponseOutputAvailableAddonsTypeIpv4: - return true - case ListProjectAddonsResponseOutputAvailableAddonsTypeLogDrain: - return true - case ListProjectAddonsResponseOutputAvailableAddonsTypePitr: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputAvailableAddonsVariantsId0. -const ( - ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci12xlarge ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 = "ci_12xlarge" - ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci16xlarge ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 = "ci_16xlarge" - ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci24xlarge ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 = "ci_24xlarge" - ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci24xlargeHighMemory ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 = "ci_24xlarge_high_memory" - ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci24xlargeOptimizedCpu ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 = "ci_24xlarge_optimized_cpu" - ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci24xlargeOptimizedMemory ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 = "ci_24xlarge_optimized_memory" - ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci2xlarge ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 = "ci_2xlarge" - ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci48xlarge ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 = "ci_48xlarge" - ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci48xlargeHighMemory ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 = "ci_48xlarge_high_memory" - ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci48xlargeOptimizedCpu ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 = "ci_48xlarge_optimized_cpu" - ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci48xlargeOptimizedMemory ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 = "ci_48xlarge_optimized_memory" - ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci4xlarge ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 = "ci_4xlarge" - ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci8xlarge ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 = "ci_8xlarge" - ListProjectAddonsResponseOutputAvailableAddonsVariantsId0CiLarge ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 = "ci_large" - ListProjectAddonsResponseOutputAvailableAddonsVariantsId0CiMedium ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 = "ci_medium" - ListProjectAddonsResponseOutputAvailableAddonsVariantsId0CiMicro ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 = "ci_micro" - ListProjectAddonsResponseOutputAvailableAddonsVariantsId0CiSmall ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 = "ci_small" - ListProjectAddonsResponseOutputAvailableAddonsVariantsId0CiXlarge ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 = "ci_xlarge" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 enum. -func (e ListProjectAddonsResponseOutputAvailableAddonsVariantsId0) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci12xlarge: - return true - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci16xlarge: - return true - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci24xlarge: - return true - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci24xlargeHighMemory: - return true - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci24xlargeOptimizedCpu: - return true - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci24xlargeOptimizedMemory: - return true - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci2xlarge: - return true - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci48xlarge: - return true - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci48xlargeHighMemory: - return true - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci48xlargeOptimizedCpu: - return true - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci48xlargeOptimizedMemory: - return true - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci4xlarge: - return true - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId0Ci8xlarge: - return true - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId0CiLarge: - return true - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId0CiMedium: - return true - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId0CiMicro: - return true - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId0CiSmall: - return true - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId0CiXlarge: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputAvailableAddonsVariantsId1. -const ( - ListProjectAddonsResponseOutputAvailableAddonsVariantsId1CdDefault ListProjectAddonsResponseOutputAvailableAddonsVariantsId1 = "cd_default" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputAvailableAddonsVariantsId1 enum. -func (e ListProjectAddonsResponseOutputAvailableAddonsVariantsId1) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId1CdDefault: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputAvailableAddonsVariantsId2. -const ( - ListProjectAddonsResponseOutputAvailableAddonsVariantsId2Pitr14 ListProjectAddonsResponseOutputAvailableAddonsVariantsId2 = "pitr_14" - ListProjectAddonsResponseOutputAvailableAddonsVariantsId2Pitr28 ListProjectAddonsResponseOutputAvailableAddonsVariantsId2 = "pitr_28" - ListProjectAddonsResponseOutputAvailableAddonsVariantsId2Pitr7 ListProjectAddonsResponseOutputAvailableAddonsVariantsId2 = "pitr_7" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputAvailableAddonsVariantsId2 enum. -func (e ListProjectAddonsResponseOutputAvailableAddonsVariantsId2) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId2Pitr14: - return true - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId2Pitr28: - return true - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId2Pitr7: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputAvailableAddonsVariantsId3. -const ( - ListProjectAddonsResponseOutputAvailableAddonsVariantsId3Ipv4Default ListProjectAddonsResponseOutputAvailableAddonsVariantsId3 = "ipv4_default" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputAvailableAddonsVariantsId3 enum. -func (e ListProjectAddonsResponseOutputAvailableAddonsVariantsId3) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId3Ipv4Default: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputAvailableAddonsVariantsId4. -const ( - ListProjectAddonsResponseOutputAvailableAddonsVariantsId4AuthMfaPhoneDefault ListProjectAddonsResponseOutputAvailableAddonsVariantsId4 = "auth_mfa_phone_default" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputAvailableAddonsVariantsId4 enum. -func (e ListProjectAddonsResponseOutputAvailableAddonsVariantsId4) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId4AuthMfaPhoneDefault: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputAvailableAddonsVariantsId5. -const ( - ListProjectAddonsResponseOutputAvailableAddonsVariantsId5AuthMfaWebAuthnDefault ListProjectAddonsResponseOutputAvailableAddonsVariantsId5 = "auth_mfa_web_authn_default" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputAvailableAddonsVariantsId5 enum. -func (e ListProjectAddonsResponseOutputAvailableAddonsVariantsId5) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId5AuthMfaWebAuthnDefault: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputAvailableAddonsVariantsId6. -const ( - ListProjectAddonsResponseOutputAvailableAddonsVariantsId6LogDrainDefault ListProjectAddonsResponseOutputAvailableAddonsVariantsId6 = "log_drain_default" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputAvailableAddonsVariantsId6 enum. -func (e ListProjectAddonsResponseOutputAvailableAddonsVariantsId6) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId6LogDrainDefault: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputAvailableAddonsVariantsId7. -const ( - ListProjectAddonsResponseOutputAvailableAddonsVariantsId7EtlPipelineDefault ListProjectAddonsResponseOutputAvailableAddonsVariantsId7 = "etl_pipeline_default" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputAvailableAddonsVariantsId7 enum. -func (e ListProjectAddonsResponseOutputAvailableAddonsVariantsId7) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputAvailableAddonsVariantsId7EtlPipelineDefault: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceInterval. -const ( - ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceIntervalHourly ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceInterval = "hourly" - ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceIntervalMonthly ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceInterval = "monthly" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceInterval enum. -func (e ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceInterval) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceIntervalHourly: - return true - case ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceIntervalMonthly: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceType. -const ( - ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceTypeFixed ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceType = "fixed" - ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceTypeUsage ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceType = "usage" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceType enum. -func (e ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceType) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceTypeFixed: - return true - case ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceTypeUsage: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputSelectedAddonsType. -const ( - ListProjectAddonsResponseOutputSelectedAddonsTypeAuthMfaPhone ListProjectAddonsResponseOutputSelectedAddonsType = "auth_mfa_phone" - ListProjectAddonsResponseOutputSelectedAddonsTypeAuthMfaWebAuthn ListProjectAddonsResponseOutputSelectedAddonsType = "auth_mfa_web_authn" - ListProjectAddonsResponseOutputSelectedAddonsTypeComputeInstance ListProjectAddonsResponseOutputSelectedAddonsType = "compute_instance" - ListProjectAddonsResponseOutputSelectedAddonsTypeCustomDomain ListProjectAddonsResponseOutputSelectedAddonsType = "custom_domain" - ListProjectAddonsResponseOutputSelectedAddonsTypeEtlPipeline ListProjectAddonsResponseOutputSelectedAddonsType = "etl_pipeline" - ListProjectAddonsResponseOutputSelectedAddonsTypeIpv4 ListProjectAddonsResponseOutputSelectedAddonsType = "ipv4" - ListProjectAddonsResponseOutputSelectedAddonsTypeLogDrain ListProjectAddonsResponseOutputSelectedAddonsType = "log_drain" - ListProjectAddonsResponseOutputSelectedAddonsTypePitr ListProjectAddonsResponseOutputSelectedAddonsType = "pitr" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputSelectedAddonsType enum. -func (e ListProjectAddonsResponseOutputSelectedAddonsType) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputSelectedAddonsTypeAuthMfaPhone: - return true - case ListProjectAddonsResponseOutputSelectedAddonsTypeAuthMfaWebAuthn: - return true - case ListProjectAddonsResponseOutputSelectedAddonsTypeComputeInstance: - return true - case ListProjectAddonsResponseOutputSelectedAddonsTypeCustomDomain: - return true - case ListProjectAddonsResponseOutputSelectedAddonsTypeEtlPipeline: - return true - case ListProjectAddonsResponseOutputSelectedAddonsTypeIpv4: - return true - case ListProjectAddonsResponseOutputSelectedAddonsTypeLogDrain: - return true - case ListProjectAddonsResponseOutputSelectedAddonsTypePitr: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputSelectedAddonsVariantId0. -const ( - ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci12xlarge ListProjectAddonsResponseOutputSelectedAddonsVariantId0 = "ci_12xlarge" - ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci16xlarge ListProjectAddonsResponseOutputSelectedAddonsVariantId0 = "ci_16xlarge" - ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci24xlarge ListProjectAddonsResponseOutputSelectedAddonsVariantId0 = "ci_24xlarge" - ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci24xlargeHighMemory ListProjectAddonsResponseOutputSelectedAddonsVariantId0 = "ci_24xlarge_high_memory" - ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci24xlargeOptimizedCpu ListProjectAddonsResponseOutputSelectedAddonsVariantId0 = "ci_24xlarge_optimized_cpu" - ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci24xlargeOptimizedMemory ListProjectAddonsResponseOutputSelectedAddonsVariantId0 = "ci_24xlarge_optimized_memory" - ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci2xlarge ListProjectAddonsResponseOutputSelectedAddonsVariantId0 = "ci_2xlarge" - ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci48xlarge ListProjectAddonsResponseOutputSelectedAddonsVariantId0 = "ci_48xlarge" - ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci48xlargeHighMemory ListProjectAddonsResponseOutputSelectedAddonsVariantId0 = "ci_48xlarge_high_memory" - ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci48xlargeOptimizedCpu ListProjectAddonsResponseOutputSelectedAddonsVariantId0 = "ci_48xlarge_optimized_cpu" - ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci48xlargeOptimizedMemory ListProjectAddonsResponseOutputSelectedAddonsVariantId0 = "ci_48xlarge_optimized_memory" - ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci4xlarge ListProjectAddonsResponseOutputSelectedAddonsVariantId0 = "ci_4xlarge" - ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci8xlarge ListProjectAddonsResponseOutputSelectedAddonsVariantId0 = "ci_8xlarge" - ListProjectAddonsResponseOutputSelectedAddonsVariantId0CiLarge ListProjectAddonsResponseOutputSelectedAddonsVariantId0 = "ci_large" - ListProjectAddonsResponseOutputSelectedAddonsVariantId0CiMedium ListProjectAddonsResponseOutputSelectedAddonsVariantId0 = "ci_medium" - ListProjectAddonsResponseOutputSelectedAddonsVariantId0CiMicro ListProjectAddonsResponseOutputSelectedAddonsVariantId0 = "ci_micro" - ListProjectAddonsResponseOutputSelectedAddonsVariantId0CiSmall ListProjectAddonsResponseOutputSelectedAddonsVariantId0 = "ci_small" - ListProjectAddonsResponseOutputSelectedAddonsVariantId0CiXlarge ListProjectAddonsResponseOutputSelectedAddonsVariantId0 = "ci_xlarge" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputSelectedAddonsVariantId0 enum. -func (e ListProjectAddonsResponseOutputSelectedAddonsVariantId0) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci12xlarge: - return true - case ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci16xlarge: - return true - case ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci24xlarge: - return true - case ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci24xlargeHighMemory: - return true - case ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci24xlargeOptimizedCpu: - return true - case ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci24xlargeOptimizedMemory: - return true - case ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci2xlarge: - return true - case ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci48xlarge: - return true - case ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci48xlargeHighMemory: - return true - case ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci48xlargeOptimizedCpu: - return true - case ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci48xlargeOptimizedMemory: - return true - case ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci4xlarge: - return true - case ListProjectAddonsResponseOutputSelectedAddonsVariantId0Ci8xlarge: - return true - case ListProjectAddonsResponseOutputSelectedAddonsVariantId0CiLarge: - return true - case ListProjectAddonsResponseOutputSelectedAddonsVariantId0CiMedium: - return true - case ListProjectAddonsResponseOutputSelectedAddonsVariantId0CiMicro: - return true - case ListProjectAddonsResponseOutputSelectedAddonsVariantId0CiSmall: - return true - case ListProjectAddonsResponseOutputSelectedAddonsVariantId0CiXlarge: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputSelectedAddonsVariantId1. -const ( - ListProjectAddonsResponseOutputSelectedAddonsVariantId1CdDefault ListProjectAddonsResponseOutputSelectedAddonsVariantId1 = "cd_default" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputSelectedAddonsVariantId1 enum. -func (e ListProjectAddonsResponseOutputSelectedAddonsVariantId1) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputSelectedAddonsVariantId1CdDefault: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputSelectedAddonsVariantId2. -const ( - ListProjectAddonsResponseOutputSelectedAddonsVariantId2Pitr14 ListProjectAddonsResponseOutputSelectedAddonsVariantId2 = "pitr_14" - ListProjectAddonsResponseOutputSelectedAddonsVariantId2Pitr28 ListProjectAddonsResponseOutputSelectedAddonsVariantId2 = "pitr_28" - ListProjectAddonsResponseOutputSelectedAddonsVariantId2Pitr7 ListProjectAddonsResponseOutputSelectedAddonsVariantId2 = "pitr_7" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputSelectedAddonsVariantId2 enum. -func (e ListProjectAddonsResponseOutputSelectedAddonsVariantId2) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputSelectedAddonsVariantId2Pitr14: - return true - case ListProjectAddonsResponseOutputSelectedAddonsVariantId2Pitr28: - return true - case ListProjectAddonsResponseOutputSelectedAddonsVariantId2Pitr7: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputSelectedAddonsVariantId3. -const ( - ListProjectAddonsResponseOutputSelectedAddonsVariantId3Ipv4Default ListProjectAddonsResponseOutputSelectedAddonsVariantId3 = "ipv4_default" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputSelectedAddonsVariantId3 enum. -func (e ListProjectAddonsResponseOutputSelectedAddonsVariantId3) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputSelectedAddonsVariantId3Ipv4Default: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputSelectedAddonsVariantId4. -const ( - ListProjectAddonsResponseOutputSelectedAddonsVariantId4AuthMfaPhoneDefault ListProjectAddonsResponseOutputSelectedAddonsVariantId4 = "auth_mfa_phone_default" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputSelectedAddonsVariantId4 enum. -func (e ListProjectAddonsResponseOutputSelectedAddonsVariantId4) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputSelectedAddonsVariantId4AuthMfaPhoneDefault: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputSelectedAddonsVariantId5. -const ( - ListProjectAddonsResponseOutputSelectedAddonsVariantId5AuthMfaWebAuthnDefault ListProjectAddonsResponseOutputSelectedAddonsVariantId5 = "auth_mfa_web_authn_default" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputSelectedAddonsVariantId5 enum. -func (e ListProjectAddonsResponseOutputSelectedAddonsVariantId5) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputSelectedAddonsVariantId5AuthMfaWebAuthnDefault: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputSelectedAddonsVariantId6. -const ( - ListProjectAddonsResponseOutputSelectedAddonsVariantId6LogDrainDefault ListProjectAddonsResponseOutputSelectedAddonsVariantId6 = "log_drain_default" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputSelectedAddonsVariantId6 enum. -func (e ListProjectAddonsResponseOutputSelectedAddonsVariantId6) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputSelectedAddonsVariantId6LogDrainDefault: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputSelectedAddonsVariantId7. -const ( - ListProjectAddonsResponseOutputSelectedAddonsVariantId7EtlPipelineDefault ListProjectAddonsResponseOutputSelectedAddonsVariantId7 = "etl_pipeline_default" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputSelectedAddonsVariantId7 enum. -func (e ListProjectAddonsResponseOutputSelectedAddonsVariantId7) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputSelectedAddonsVariantId7EtlPipelineDefault: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputSelectedAddonsVariantPriceInterval. -const ( - ListProjectAddonsResponseOutputSelectedAddonsVariantPriceIntervalHourly ListProjectAddonsResponseOutputSelectedAddonsVariantPriceInterval = "hourly" - ListProjectAddonsResponseOutputSelectedAddonsVariantPriceIntervalMonthly ListProjectAddonsResponseOutputSelectedAddonsVariantPriceInterval = "monthly" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputSelectedAddonsVariantPriceInterval enum. -func (e ListProjectAddonsResponseOutputSelectedAddonsVariantPriceInterval) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputSelectedAddonsVariantPriceIntervalHourly: - return true - case ListProjectAddonsResponseOutputSelectedAddonsVariantPriceIntervalMonthly: - return true - default: - return false - } -} - -// Defines values for ListProjectAddonsResponseOutputSelectedAddonsVariantPriceType. -const ( - ListProjectAddonsResponseOutputSelectedAddonsVariantPriceTypeFixed ListProjectAddonsResponseOutputSelectedAddonsVariantPriceType = "fixed" - ListProjectAddonsResponseOutputSelectedAddonsVariantPriceTypeUsage ListProjectAddonsResponseOutputSelectedAddonsVariantPriceType = "usage" -) - -// Valid indicates whether the value is a known member of the ListProjectAddonsResponseOutputSelectedAddonsVariantPriceType enum. -func (e ListProjectAddonsResponseOutputSelectedAddonsVariantPriceType) Valid() bool { - switch e { - case ListProjectAddonsResponseOutputSelectedAddonsVariantPriceTypeFixed: - return true - case ListProjectAddonsResponseOutputSelectedAddonsVariantPriceTypeUsage: - return true - default: - return false - } -} - -// Defines values for NetworkRestrictionsResponseOutputEntitlement. -const ( - NetworkRestrictionsResponseOutputEntitlementAllowed NetworkRestrictionsResponseOutputEntitlement = "allowed" - NetworkRestrictionsResponseOutputEntitlementDisallowed NetworkRestrictionsResponseOutputEntitlement = "disallowed" -) - -// Valid indicates whether the value is a known member of the NetworkRestrictionsResponseOutputEntitlement enum. -func (e NetworkRestrictionsResponseOutputEntitlement) Valid() bool { - switch e { - case NetworkRestrictionsResponseOutputEntitlementAllowed: - return true - case NetworkRestrictionsResponseOutputEntitlementDisallowed: - return true - default: - return false - } -} - -// Defines values for NetworkRestrictionsResponseOutputStatus. -const ( - NetworkRestrictionsResponseOutputStatusApplied NetworkRestrictionsResponseOutputStatus = "applied" - NetworkRestrictionsResponseOutputStatusStored NetworkRestrictionsResponseOutputStatus = "stored" -) - -// Valid indicates whether the value is a known member of the NetworkRestrictionsResponseOutputStatus enum. -func (e NetworkRestrictionsResponseOutputStatus) Valid() bool { - switch e { - case NetworkRestrictionsResponseOutputStatusApplied: - return true - case NetworkRestrictionsResponseOutputStatusStored: - return true - default: - return false - } -} - -// Defines values for NetworkRestrictionsV2ResponseOutputConfigDbAllowedCidrsType. -const ( - NetworkRestrictionsV2ResponseOutputConfigDbAllowedCidrsTypeV4 NetworkRestrictionsV2ResponseOutputConfigDbAllowedCidrsType = "v4" - NetworkRestrictionsV2ResponseOutputConfigDbAllowedCidrsTypeV6 NetworkRestrictionsV2ResponseOutputConfigDbAllowedCidrsType = "v6" -) - -// Valid indicates whether the value is a known member of the NetworkRestrictionsV2ResponseOutputConfigDbAllowedCidrsType enum. -func (e NetworkRestrictionsV2ResponseOutputConfigDbAllowedCidrsType) Valid() bool { - switch e { - case NetworkRestrictionsV2ResponseOutputConfigDbAllowedCidrsTypeV4: - return true - case NetworkRestrictionsV2ResponseOutputConfigDbAllowedCidrsTypeV6: - return true - default: - return false - } -} - -// Defines values for NetworkRestrictionsV2ResponseOutputEntitlement. -const ( - NetworkRestrictionsV2ResponseOutputEntitlementAllowed NetworkRestrictionsV2ResponseOutputEntitlement = "allowed" - NetworkRestrictionsV2ResponseOutputEntitlementDisallowed NetworkRestrictionsV2ResponseOutputEntitlement = "disallowed" -) - -// Valid indicates whether the value is a known member of the NetworkRestrictionsV2ResponseOutputEntitlement enum. -func (e NetworkRestrictionsV2ResponseOutputEntitlement) Valid() bool { - switch e { - case NetworkRestrictionsV2ResponseOutputEntitlementAllowed: - return true - case NetworkRestrictionsV2ResponseOutputEntitlementDisallowed: - return true - default: - return false - } -} - -// Defines values for NetworkRestrictionsV2ResponseOutputOldConfigDbAllowedCidrsType. -const ( - NetworkRestrictionsV2ResponseOutputOldConfigDbAllowedCidrsTypeV4 NetworkRestrictionsV2ResponseOutputOldConfigDbAllowedCidrsType = "v4" - NetworkRestrictionsV2ResponseOutputOldConfigDbAllowedCidrsTypeV6 NetworkRestrictionsV2ResponseOutputOldConfigDbAllowedCidrsType = "v6" -) - -// Valid indicates whether the value is a known member of the NetworkRestrictionsV2ResponseOutputOldConfigDbAllowedCidrsType enum. -func (e NetworkRestrictionsV2ResponseOutputOldConfigDbAllowedCidrsType) Valid() bool { - switch e { - case NetworkRestrictionsV2ResponseOutputOldConfigDbAllowedCidrsTypeV4: - return true - case NetworkRestrictionsV2ResponseOutputOldConfigDbAllowedCidrsTypeV6: - return true - default: - return false - } -} - -// Defines values for NetworkRestrictionsV2ResponseOutputStatus. -const ( - NetworkRestrictionsV2ResponseOutputStatusApplied NetworkRestrictionsV2ResponseOutputStatus = "applied" - NetworkRestrictionsV2ResponseOutputStatusStored NetworkRestrictionsV2ResponseOutputStatus = "stored" -) - -// Valid indicates whether the value is a known member of the NetworkRestrictionsV2ResponseOutputStatus enum. -func (e NetworkRestrictionsV2ResponseOutputStatus) Valid() bool { - switch e { - case NetworkRestrictionsV2ResponseOutputStatusApplied: - return true - case NetworkRestrictionsV2ResponseOutputStatusStored: - return true - default: - return false - } -} - -// Defines values for OAuthTokenBodyGrantType. -const ( - AuthorizationCode OAuthTokenBodyGrantType = "authorization_code" - RefreshToken OAuthTokenBodyGrantType = "refresh_token" - UrnIetfParamsOauthGrantTypeJwtBearer OAuthTokenBodyGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" -) - -// Valid indicates whether the value is a known member of the OAuthTokenBodyGrantType enum. -func (e OAuthTokenBodyGrantType) Valid() bool { - switch e { - case AuthorizationCode: - return true - case RefreshToken: - return true - case UrnIetfParamsOauthGrantTypeJwtBearer: - return true - default: - return false - } -} - -// Defines values for OAuthTokenResponseOutputTokenType. -const ( - Bearer OAuthTokenResponseOutputTokenType = "Bearer" -) - -// Valid indicates whether the value is a known member of the OAuthTokenResponseOutputTokenType enum. -func (e OAuthTokenResponseOutputTokenType) Valid() bool { - switch e { - case Bearer: - return true - default: - return false - } -} - -// Defines values for OrganizationProjectClaimResponseOutputPreviewSourceSubscriptionPlan. -const ( - OrganizationProjectClaimResponseOutputPreviewSourceSubscriptionPlanEnterprise OrganizationProjectClaimResponseOutputPreviewSourceSubscriptionPlan = "enterprise" - OrganizationProjectClaimResponseOutputPreviewSourceSubscriptionPlanFree OrganizationProjectClaimResponseOutputPreviewSourceSubscriptionPlan = "free" - OrganizationProjectClaimResponseOutputPreviewSourceSubscriptionPlanPlatform OrganizationProjectClaimResponseOutputPreviewSourceSubscriptionPlan = "platform" - OrganizationProjectClaimResponseOutputPreviewSourceSubscriptionPlanPro OrganizationProjectClaimResponseOutputPreviewSourceSubscriptionPlan = "pro" - OrganizationProjectClaimResponseOutputPreviewSourceSubscriptionPlanTeam OrganizationProjectClaimResponseOutputPreviewSourceSubscriptionPlan = "team" -) - -// Valid indicates whether the value is a known member of the OrganizationProjectClaimResponseOutputPreviewSourceSubscriptionPlan enum. -func (e OrganizationProjectClaimResponseOutputPreviewSourceSubscriptionPlan) Valid() bool { - switch e { - case OrganizationProjectClaimResponseOutputPreviewSourceSubscriptionPlanEnterprise: - return true - case OrganizationProjectClaimResponseOutputPreviewSourceSubscriptionPlanFree: - return true - case OrganizationProjectClaimResponseOutputPreviewSourceSubscriptionPlanPlatform: - return true - case OrganizationProjectClaimResponseOutputPreviewSourceSubscriptionPlanPro: - return true - case OrganizationProjectClaimResponseOutputPreviewSourceSubscriptionPlanTeam: - return true - default: - return false - } -} - -// Defines values for OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlan. -const ( - OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlanEnterprise OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlan = "enterprise" - OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlanFree OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlan = "free" - OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlanLessThannil OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlan = "" - OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlanPlatform OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlan = "platform" - OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlanPro OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlan = "pro" - OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlanTeam OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlan = "team" -) - -// Valid indicates whether the value is a known member of the OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlan enum. -func (e OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlan) Valid() bool { - switch e { - case OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlanEnterprise: - return true - case OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlanFree: - return true - case OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlanLessThannil: - return true - case OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlanPlatform: - return true - case OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlanPro: - return true - case OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlanTeam: - return true - default: - return false - } -} - -// Defines values for OrganizationProjectsResponseOutputProjectsDatabasesDiskType. -const ( - Gp3 OrganizationProjectsResponseOutputProjectsDatabasesDiskType = "gp3" - Io2 OrganizationProjectsResponseOutputProjectsDatabasesDiskType = "io2" -) - -// Valid indicates whether the value is a known member of the OrganizationProjectsResponseOutputProjectsDatabasesDiskType enum. -func (e OrganizationProjectsResponseOutputProjectsDatabasesDiskType) Valid() bool { - switch e { - case Gp3: - return true - case Io2: - return true - default: - return false - } -} - -// Defines values for OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize. -const ( - OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeLarge OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize = "large" - OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeMedium OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize = "medium" - OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeMicro OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize = "micro" - OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN12xlarge OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize = "12xlarge" - OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN16xlarge OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize = "16xlarge" - OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN24xlarge OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize = "24xlarge" - OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN24xlargeHighMemory OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize = "24xlarge_high_memory" - OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN24xlargeOptimizedCpu OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize = "24xlarge_optimized_cpu" - OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN24xlargeOptimizedMemory OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize = "24xlarge_optimized_memory" - OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN2xlarge OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize = "2xlarge" - OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN48xlarge OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize = "48xlarge" - OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN48xlargeHighMemory OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize = "48xlarge_high_memory" - OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN48xlargeOptimizedCpu OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize = "48xlarge_optimized_cpu" - OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN48xlargeOptimizedMemory OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize = "48xlarge_optimized_memory" - OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN4xlarge OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize = "4xlarge" - OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN8xlarge OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize = "8xlarge" - OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeNano OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize = "nano" - OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizePico OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize = "pico" - OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeSmall OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize = "small" - OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeXlarge OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize = "xlarge" -) - -// Valid indicates whether the value is a known member of the OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize enum. -func (e OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize) Valid() bool { - switch e { - case OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeLarge: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeMedium: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeMicro: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN12xlarge: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN16xlarge: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN24xlarge: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN24xlargeHighMemory: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN24xlargeOptimizedCpu: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN24xlargeOptimizedMemory: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN2xlarge: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN48xlarge: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN48xlargeHighMemory: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN48xlargeOptimizedCpu: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN48xlargeOptimizedMemory: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN4xlarge: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeN8xlarge: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeNano: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizePico: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeSmall: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSizeXlarge: - return true - default: - return false - } -} - -// Defines values for OrganizationProjectsResponseOutputProjectsDatabasesStatus. -const ( - OrganizationProjectsResponseOutputProjectsDatabasesStatusACTIVEHEALTHY OrganizationProjectsResponseOutputProjectsDatabasesStatus = "ACTIVE_HEALTHY" - OrganizationProjectsResponseOutputProjectsDatabasesStatusACTIVEUNHEALTHY OrganizationProjectsResponseOutputProjectsDatabasesStatus = "ACTIVE_UNHEALTHY" - OrganizationProjectsResponseOutputProjectsDatabasesStatusCOMINGUP OrganizationProjectsResponseOutputProjectsDatabasesStatus = "COMING_UP" - OrganizationProjectsResponseOutputProjectsDatabasesStatusGOINGDOWN OrganizationProjectsResponseOutputProjectsDatabasesStatus = "GOING_DOWN" - OrganizationProjectsResponseOutputProjectsDatabasesStatusINITFAILED OrganizationProjectsResponseOutputProjectsDatabasesStatus = "INIT_FAILED" - OrganizationProjectsResponseOutputProjectsDatabasesStatusINITREADREPLICA OrganizationProjectsResponseOutputProjectsDatabasesStatus = "INIT_READ_REPLICA" - OrganizationProjectsResponseOutputProjectsDatabasesStatusINITREADREPLICAFAILED OrganizationProjectsResponseOutputProjectsDatabasesStatus = "INIT_READ_REPLICA_FAILED" - OrganizationProjectsResponseOutputProjectsDatabasesStatusREMOVED OrganizationProjectsResponseOutputProjectsDatabasesStatus = "REMOVED" - OrganizationProjectsResponseOutputProjectsDatabasesStatusRESIZING OrganizationProjectsResponseOutputProjectsDatabasesStatus = "RESIZING" - OrganizationProjectsResponseOutputProjectsDatabasesStatusRESTARTING OrganizationProjectsResponseOutputProjectsDatabasesStatus = "RESTARTING" - OrganizationProjectsResponseOutputProjectsDatabasesStatusRESTORING OrganizationProjectsResponseOutputProjectsDatabasesStatus = "RESTORING" - OrganizationProjectsResponseOutputProjectsDatabasesStatusUNKNOWN OrganizationProjectsResponseOutputProjectsDatabasesStatus = "UNKNOWN" -) - -// Valid indicates whether the value is a known member of the OrganizationProjectsResponseOutputProjectsDatabasesStatus enum. -func (e OrganizationProjectsResponseOutputProjectsDatabasesStatus) Valid() bool { - switch e { - case OrganizationProjectsResponseOutputProjectsDatabasesStatusACTIVEHEALTHY: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesStatusACTIVEUNHEALTHY: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesStatusCOMINGUP: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesStatusGOINGDOWN: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesStatusINITFAILED: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesStatusINITREADREPLICA: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesStatusINITREADREPLICAFAILED: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesStatusREMOVED: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesStatusRESIZING: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesStatusRESTARTING: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesStatusRESTORING: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesStatusUNKNOWN: - return true - default: - return false - } -} - -// Defines values for OrganizationProjectsResponseOutputProjectsDatabasesType. -const ( - OrganizationProjectsResponseOutputProjectsDatabasesTypePRIMARY OrganizationProjectsResponseOutputProjectsDatabasesType = "PRIMARY" - OrganizationProjectsResponseOutputProjectsDatabasesTypeREADREPLICA OrganizationProjectsResponseOutputProjectsDatabasesType = "READ_REPLICA" -) - -// Valid indicates whether the value is a known member of the OrganizationProjectsResponseOutputProjectsDatabasesType enum. -func (e OrganizationProjectsResponseOutputProjectsDatabasesType) Valid() bool { - switch e { - case OrganizationProjectsResponseOutputProjectsDatabasesTypePRIMARY: - return true - case OrganizationProjectsResponseOutputProjectsDatabasesTypeREADREPLICA: - return true - default: - return false - } -} - -// Defines values for OrganizationProjectsResponseOutputProjectsStatus. -const ( - OrganizationProjectsResponseOutputProjectsStatusACTIVEHEALTHY OrganizationProjectsResponseOutputProjectsStatus = "ACTIVE_HEALTHY" - OrganizationProjectsResponseOutputProjectsStatusACTIVEUNHEALTHY OrganizationProjectsResponseOutputProjectsStatus = "ACTIVE_UNHEALTHY" - OrganizationProjectsResponseOutputProjectsStatusCOMINGUP OrganizationProjectsResponseOutputProjectsStatus = "COMING_UP" - OrganizationProjectsResponseOutputProjectsStatusGOINGDOWN OrganizationProjectsResponseOutputProjectsStatus = "GOING_DOWN" - OrganizationProjectsResponseOutputProjectsStatusINACTIVE OrganizationProjectsResponseOutputProjectsStatus = "INACTIVE" - OrganizationProjectsResponseOutputProjectsStatusINITFAILED OrganizationProjectsResponseOutputProjectsStatus = "INIT_FAILED" - OrganizationProjectsResponseOutputProjectsStatusPAUSEFAILED OrganizationProjectsResponseOutputProjectsStatus = "PAUSE_FAILED" - OrganizationProjectsResponseOutputProjectsStatusPAUSING OrganizationProjectsResponseOutputProjectsStatus = "PAUSING" - OrganizationProjectsResponseOutputProjectsStatusREMOVED OrganizationProjectsResponseOutputProjectsStatus = "REMOVED" - OrganizationProjectsResponseOutputProjectsStatusRESIZING OrganizationProjectsResponseOutputProjectsStatus = "RESIZING" - OrganizationProjectsResponseOutputProjectsStatusRESTARTING OrganizationProjectsResponseOutputProjectsStatus = "RESTARTING" - OrganizationProjectsResponseOutputProjectsStatusRESTOREFAILED OrganizationProjectsResponseOutputProjectsStatus = "RESTORE_FAILED" - OrganizationProjectsResponseOutputProjectsStatusRESTORING OrganizationProjectsResponseOutputProjectsStatus = "RESTORING" - OrganizationProjectsResponseOutputProjectsStatusUNKNOWN OrganizationProjectsResponseOutputProjectsStatus = "UNKNOWN" - OrganizationProjectsResponseOutputProjectsStatusUPGRADING OrganizationProjectsResponseOutputProjectsStatus = "UPGRADING" -) - -// Valid indicates whether the value is a known member of the OrganizationProjectsResponseOutputProjectsStatus enum. -func (e OrganizationProjectsResponseOutputProjectsStatus) Valid() bool { - switch e { - case OrganizationProjectsResponseOutputProjectsStatusACTIVEHEALTHY: - return true - case OrganizationProjectsResponseOutputProjectsStatusACTIVEUNHEALTHY: - return true - case OrganizationProjectsResponseOutputProjectsStatusCOMINGUP: - return true - case OrganizationProjectsResponseOutputProjectsStatusGOINGDOWN: - return true - case OrganizationProjectsResponseOutputProjectsStatusINACTIVE: - return true - case OrganizationProjectsResponseOutputProjectsStatusINITFAILED: - return true - case OrganizationProjectsResponseOutputProjectsStatusPAUSEFAILED: - return true - case OrganizationProjectsResponseOutputProjectsStatusPAUSING: - return true - case OrganizationProjectsResponseOutputProjectsStatusREMOVED: - return true - case OrganizationProjectsResponseOutputProjectsStatusRESIZING: - return true - case OrganizationProjectsResponseOutputProjectsStatusRESTARTING: - return true - case OrganizationProjectsResponseOutputProjectsStatusRESTOREFAILED: - return true - case OrganizationProjectsResponseOutputProjectsStatusRESTORING: - return true - case OrganizationProjectsResponseOutputProjectsStatusUNKNOWN: - return true - case OrganizationProjectsResponseOutputProjectsStatusUPGRADING: - return true - default: - return false - } -} - -// Defines values for PlanGateErrorBodyErrorCode. -const ( - EntitlementRequired PlanGateErrorBodyErrorCode = "entitlement_required" -) - -// Valid indicates whether the value is a known member of the PlanGateErrorBodyErrorCode enum. -func (e PlanGateErrorBodyErrorCode) Valid() bool { - switch e { - case EntitlementRequired: - return true - default: - return false - } -} - -// Defines values for PostgresConfigResponseOutputSessionReplicationRole. -const ( - PostgresConfigResponseOutputSessionReplicationRoleLocal PostgresConfigResponseOutputSessionReplicationRole = "local" - PostgresConfigResponseOutputSessionReplicationRoleOrigin PostgresConfigResponseOutputSessionReplicationRole = "origin" - PostgresConfigResponseOutputSessionReplicationRoleReplica PostgresConfigResponseOutputSessionReplicationRole = "replica" -) - -// Valid indicates whether the value is a known member of the PostgresConfigResponseOutputSessionReplicationRole enum. -func (e PostgresConfigResponseOutputSessionReplicationRole) Valid() bool { - switch e { - case PostgresConfigResponseOutputSessionReplicationRoleLocal: - return true - case PostgresConfigResponseOutputSessionReplicationRoleOrigin: - return true - case PostgresConfigResponseOutputSessionReplicationRoleReplica: - return true - default: - return false - } -} - -// Defines values for ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannel. -const ( - ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannelAlpha ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannel = "alpha" - ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannelBeta ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannel = "beta" - ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannelGa ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannel = "ga" - ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannelInternal ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannel = "internal" - ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannelPreview ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannel = "preview" - ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannelWithdrawn ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannel = "withdrawn" -) - -// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannel enum. -func (e ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannel) Valid() bool { - switch e { - case ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannelAlpha: - return true - case ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannelBeta: - return true - case ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannelGa: - return true - case ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannelInternal: - return true - case ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannelPreview: - return true - case ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannelWithdrawn: - return true - default: - return false - } -} - -// Defines values for ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsPostgresVersion. -const ( - N13 ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsPostgresVersion = "13" - N14 ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsPostgresVersion = "14" - N15 ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsPostgresVersion = "15" - N17 ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsPostgresVersion = "17" - N17Oriole ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsPostgresVersion = "17-oriole" -) - -// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsPostgresVersion enum. -func (e ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsPostgresVersion) Valid() bool { - switch e { - case N13: - return true - case N14: - return true - case N15: - return true - case N17: - return true - case N17Oriole: - return true - default: - return false - } -} - -// Defines values for ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannel. -const ( - ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannelAlpha ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannel = "alpha" - ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannelBeta ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannel = "beta" - ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannelGa ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannel = "ga" - ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannelInternal ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannel = "internal" - ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannelPreview ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannel = "preview" - ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannelWithdrawn ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannel = "withdrawn" -) - -// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannel enum. -func (e ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannel) Valid() bool { - switch e { - case ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannelAlpha: - return true - case ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannelBeta: - return true - case ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannelGa: - return true - case ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannelInternal: - return true - case ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannelPreview: - return true - case ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannelWithdrawn: - return true - default: - return false - } -} - -// Defines values for ProjectUpgradeEligibilityResponseOutputValidationErrors0Type. -const ( - ObjectsDependingOnPgCron ProjectUpgradeEligibilityResponseOutputValidationErrors0Type = "objects_depending_on_pg_cron" -) - -// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseOutputValidationErrors0Type enum. -func (e ProjectUpgradeEligibilityResponseOutputValidationErrors0Type) Valid() bool { - switch e { - case ObjectsDependingOnPgCron: - return true - default: - return false - } -} - -// Defines values for ProjectUpgradeEligibilityResponseOutputValidationErrors1Type. -const ( - IndexesReferencingLlToEarth ProjectUpgradeEligibilityResponseOutputValidationErrors1Type = "indexes_referencing_ll_to_earth" -) - -// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseOutputValidationErrors1Type enum. -func (e ProjectUpgradeEligibilityResponseOutputValidationErrors1Type) Valid() bool { - switch e { - case IndexesReferencingLlToEarth: - return true - default: - return false - } -} - -// Defines values for ProjectUpgradeEligibilityResponseOutputValidationErrors2Type. -const ( - FunctionUsingObsoleteLang ProjectUpgradeEligibilityResponseOutputValidationErrors2Type = "function_using_obsolete_lang" -) - -// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseOutputValidationErrors2Type enum. -func (e ProjectUpgradeEligibilityResponseOutputValidationErrors2Type) Valid() bool { - switch e { - case FunctionUsingObsoleteLang: - return true - default: - return false - } -} - -// Defines values for ProjectUpgradeEligibilityResponseOutputValidationErrors3Type. -const ( - UnsupportedExtension ProjectUpgradeEligibilityResponseOutputValidationErrors3Type = "unsupported_extension" -) - -// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseOutputValidationErrors3Type enum. -func (e ProjectUpgradeEligibilityResponseOutputValidationErrors3Type) Valid() bool { - switch e { - case UnsupportedExtension: - return true - default: - return false - } -} - -// Defines values for ProjectUpgradeEligibilityResponseOutputValidationErrors4Type. -const ( - UnsupportedFdwHandler ProjectUpgradeEligibilityResponseOutputValidationErrors4Type = "unsupported_fdw_handler" -) - -// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseOutputValidationErrors4Type enum. -func (e ProjectUpgradeEligibilityResponseOutputValidationErrors4Type) Valid() bool { - switch e { - case UnsupportedFdwHandler: - return true - default: - return false - } -} - -// Defines values for ProjectUpgradeEligibilityResponseOutputValidationErrors5Type. -const ( - UnloggedTableWithPersistentSequence ProjectUpgradeEligibilityResponseOutputValidationErrors5Type = "unlogged_table_with_persistent_sequence" -) - -// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseOutputValidationErrors5Type enum. -func (e ProjectUpgradeEligibilityResponseOutputValidationErrors5Type) Valid() bool { - switch e { - case UnloggedTableWithPersistentSequence: - return true - default: - return false - } -} - -// Defines values for ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType0. -const ( - ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType0Table ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType0 = "table" -) - -// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType0 enum. -func (e ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType0) Valid() bool { - switch e { - case ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType0Table: - return true - default: - return false - } -} - -// Defines values for ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType1. -const ( - Function ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType1 = "function" -) - -// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType1 enum. -func (e ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType1) Valid() bool { - switch e { - case Function: - return true - default: - return false - } -} - -// Defines values for ProjectUpgradeEligibilityResponseOutputValidationErrors6Type. -const ( - UserDefinedObjectsInInternalSchemas ProjectUpgradeEligibilityResponseOutputValidationErrors6Type = "user_defined_objects_in_internal_schemas" -) - -// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseOutputValidationErrors6Type enum. -func (e ProjectUpgradeEligibilityResponseOutputValidationErrors6Type) Valid() bool { - switch e { - case UserDefinedObjectsInInternalSchemas: - return true - default: - return false - } -} - -// Defines values for ProjectUpgradeEligibilityResponseOutputValidationErrors7Type. -const ( - ActiveReplicationSlot ProjectUpgradeEligibilityResponseOutputValidationErrors7Type = "active_replication_slot" -) - -// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseOutputValidationErrors7Type enum. -func (e ProjectUpgradeEligibilityResponseOutputValidationErrors7Type) Valid() bool { - switch e { - case ActiveReplicationSlot: - return true - default: - return false - } -} - -// Defines values for ProjectUpgradeEligibilityResponseOutputValidationErrors8Type. -const ( - X86Architecture ProjectUpgradeEligibilityResponseOutputValidationErrors8Type = "x86_architecture" -) - -// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseOutputValidationErrors8Type enum. -func (e ProjectUpgradeEligibilityResponseOutputValidationErrors8Type) Valid() bool { - switch e { - case X86Architecture: - return true - default: - return false - } -} - -// Defines values for ProjectUpgradeEligibilityResponseOutputValidationErrors9Type. -const ( - ProjectHibernating ProjectUpgradeEligibilityResponseOutputValidationErrors9Type = "project_hibernating" -) - -// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseOutputValidationErrors9Type enum. -func (e ProjectUpgradeEligibilityResponseOutputValidationErrors9Type) Valid() bool { - switch e { - case ProjectHibernating: - return true - default: - return false - } -} - -// Defines values for ProjectUpgradeEligibilityResponseOutputWarnings0Type. -const ( - PgGraphqlIntrospectionChange ProjectUpgradeEligibilityResponseOutputWarnings0Type = "pg_graphql_introspection_change" -) - -// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseOutputWarnings0Type enum. -func (e ProjectUpgradeEligibilityResponseOutputWarnings0Type) Valid() bool { - switch e { - case PgGraphqlIntrospectionChange: - return true - default: - return false - } -} - -// Defines values for ProjectUpgradeEligibilityResponseOutputWarnings1Type. -const ( - LtreeReindexRequired ProjectUpgradeEligibilityResponseOutputWarnings1Type = "ltree_reindex_required" -) - -// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseOutputWarnings1Type enum. -func (e ProjectUpgradeEligibilityResponseOutputWarnings1Type) Valid() bool { - switch e { - case LtreeReindexRequired: - return true - default: - return false - } -} - -// Defines values for ProjectUpgradeEligibilityResponseOutputWarnings2Type. -const ( - OperatorEstimatorGate ProjectUpgradeEligibilityResponseOutputWarnings2Type = "operator_estimator_gate" -) - -// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseOutputWarnings2Type enum. -func (e ProjectUpgradeEligibilityResponseOutputWarnings2Type) Valid() bool { - switch e { - case OperatorEstimatorGate: - return true - default: - return false - } -} - -// Defines values for ProjectUpgradeEligibilityResponseOutputWarnings3Type. -const ( - BtreeGistNanReindex ProjectUpgradeEligibilityResponseOutputWarnings3Type = "btree_gist_nan_reindex" -) - -// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseOutputWarnings3Type enum. -func (e ProjectUpgradeEligibilityResponseOutputWarnings3Type) Valid() bool { - switch e { - case BtreeGistNanReindex: - return true - default: - return false - } -} - -// Defines values for RegionsInfoV1OutputAllSmartGroupCode. -const ( - RegionsInfoV1OutputAllSmartGroupCodeAmericas RegionsInfoV1OutputAllSmartGroupCode = "americas" - RegionsInfoV1OutputAllSmartGroupCodeApac RegionsInfoV1OutputAllSmartGroupCode = "apac" - RegionsInfoV1OutputAllSmartGroupCodeEmea RegionsInfoV1OutputAllSmartGroupCode = "emea" -) - -// Valid indicates whether the value is a known member of the RegionsInfoV1OutputAllSmartGroupCode enum. -func (e RegionsInfoV1OutputAllSmartGroupCode) Valid() bool { - switch e { - case RegionsInfoV1OutputAllSmartGroupCodeAmericas: - return true - case RegionsInfoV1OutputAllSmartGroupCodeApac: - return true - case RegionsInfoV1OutputAllSmartGroupCodeEmea: - return true - default: - return false - } -} - -// Defines values for RegionsInfoV1OutputAllSmartGroupType. -const ( - RegionsInfoV1OutputAllSmartGroupTypeSmartGroup RegionsInfoV1OutputAllSmartGroupType = "smartGroup" -) - -// Valid indicates whether the value is a known member of the RegionsInfoV1OutputAllSmartGroupType enum. -func (e RegionsInfoV1OutputAllSmartGroupType) Valid() bool { - switch e { - case RegionsInfoV1OutputAllSmartGroupTypeSmartGroup: - return true - default: - return false - } -} - -// Defines values for RegionsInfoV1OutputAllSpecificCode. -const ( - RegionsInfoV1OutputAllSpecificCodeApEast1 RegionsInfoV1OutputAllSpecificCode = "ap-east-1" - RegionsInfoV1OutputAllSpecificCodeApNortheast1 RegionsInfoV1OutputAllSpecificCode = "ap-northeast-1" - RegionsInfoV1OutputAllSpecificCodeApNortheast2 RegionsInfoV1OutputAllSpecificCode = "ap-northeast-2" - RegionsInfoV1OutputAllSpecificCodeApSouth1 RegionsInfoV1OutputAllSpecificCode = "ap-south-1" - RegionsInfoV1OutputAllSpecificCodeApSoutheast1 RegionsInfoV1OutputAllSpecificCode = "ap-southeast-1" - RegionsInfoV1OutputAllSpecificCodeApSoutheast2 RegionsInfoV1OutputAllSpecificCode = "ap-southeast-2" - RegionsInfoV1OutputAllSpecificCodeCaCentral1 RegionsInfoV1OutputAllSpecificCode = "ca-central-1" - RegionsInfoV1OutputAllSpecificCodeEuCentral1 RegionsInfoV1OutputAllSpecificCode = "eu-central-1" - RegionsInfoV1OutputAllSpecificCodeEuCentral2 RegionsInfoV1OutputAllSpecificCode = "eu-central-2" - RegionsInfoV1OutputAllSpecificCodeEuNorth1 RegionsInfoV1OutputAllSpecificCode = "eu-north-1" - RegionsInfoV1OutputAllSpecificCodeEuWest1 RegionsInfoV1OutputAllSpecificCode = "eu-west-1" - RegionsInfoV1OutputAllSpecificCodeEuWest2 RegionsInfoV1OutputAllSpecificCode = "eu-west-2" - RegionsInfoV1OutputAllSpecificCodeEuWest3 RegionsInfoV1OutputAllSpecificCode = "eu-west-3" - RegionsInfoV1OutputAllSpecificCodeSaEast1 RegionsInfoV1OutputAllSpecificCode = "sa-east-1" - RegionsInfoV1OutputAllSpecificCodeUsEast1 RegionsInfoV1OutputAllSpecificCode = "us-east-1" - RegionsInfoV1OutputAllSpecificCodeUsEast2 RegionsInfoV1OutputAllSpecificCode = "us-east-2" - RegionsInfoV1OutputAllSpecificCodeUsWest1 RegionsInfoV1OutputAllSpecificCode = "us-west-1" - RegionsInfoV1OutputAllSpecificCodeUsWest2 RegionsInfoV1OutputAllSpecificCode = "us-west-2" -) - -// Valid indicates whether the value is a known member of the RegionsInfoV1OutputAllSpecificCode enum. -func (e RegionsInfoV1OutputAllSpecificCode) Valid() bool { - switch e { - case RegionsInfoV1OutputAllSpecificCodeApEast1: - return true - case RegionsInfoV1OutputAllSpecificCodeApNortheast1: - return true - case RegionsInfoV1OutputAllSpecificCodeApNortheast2: - return true - case RegionsInfoV1OutputAllSpecificCodeApSouth1: - return true - case RegionsInfoV1OutputAllSpecificCodeApSoutheast1: - return true - case RegionsInfoV1OutputAllSpecificCodeApSoutheast2: - return true - case RegionsInfoV1OutputAllSpecificCodeCaCentral1: - return true - case RegionsInfoV1OutputAllSpecificCodeEuCentral1: - return true - case RegionsInfoV1OutputAllSpecificCodeEuCentral2: - return true - case RegionsInfoV1OutputAllSpecificCodeEuNorth1: - return true - case RegionsInfoV1OutputAllSpecificCodeEuWest1: - return true - case RegionsInfoV1OutputAllSpecificCodeEuWest2: - return true - case RegionsInfoV1OutputAllSpecificCodeEuWest3: - return true - case RegionsInfoV1OutputAllSpecificCodeSaEast1: - return true - case RegionsInfoV1OutputAllSpecificCodeUsEast1: - return true - case RegionsInfoV1OutputAllSpecificCodeUsEast2: - return true - case RegionsInfoV1OutputAllSpecificCodeUsWest1: - return true - case RegionsInfoV1OutputAllSpecificCodeUsWest2: - return true - default: - return false - } -} - -// Defines values for RegionsInfoV1OutputAllSpecificProvider. -const ( - RegionsInfoV1OutputAllSpecificProviderAWS RegionsInfoV1OutputAllSpecificProvider = "AWS" - RegionsInfoV1OutputAllSpecificProviderAWSK8S RegionsInfoV1OutputAllSpecificProvider = "AWS_K8S" - RegionsInfoV1OutputAllSpecificProviderAWSNIMBUS RegionsInfoV1OutputAllSpecificProvider = "AWS_NIMBUS" -) - -// Valid indicates whether the value is a known member of the RegionsInfoV1OutputAllSpecificProvider enum. -func (e RegionsInfoV1OutputAllSpecificProvider) Valid() bool { - switch e { - case RegionsInfoV1OutputAllSpecificProviderAWS: - return true - case RegionsInfoV1OutputAllSpecificProviderAWSK8S: - return true - case RegionsInfoV1OutputAllSpecificProviderAWSNIMBUS: - return true - default: - return false - } -} - -// Defines values for RegionsInfoV1OutputAllSpecificStatus. -const ( - RegionsInfoV1OutputAllSpecificStatusCapacity RegionsInfoV1OutputAllSpecificStatus = "capacity" - RegionsInfoV1OutputAllSpecificStatusOther RegionsInfoV1OutputAllSpecificStatus = "other" -) - -// Valid indicates whether the value is a known member of the RegionsInfoV1OutputAllSpecificStatus enum. -func (e RegionsInfoV1OutputAllSpecificStatus) Valid() bool { - switch e { - case RegionsInfoV1OutputAllSpecificStatusCapacity: - return true - case RegionsInfoV1OutputAllSpecificStatusOther: - return true - default: - return false - } -} - -// Defines values for RegionsInfoV1OutputAllSpecificType. -const ( - RegionsInfoV1OutputAllSpecificTypeSpecific RegionsInfoV1OutputAllSpecificType = "specific" -) - -// Valid indicates whether the value is a known member of the RegionsInfoV1OutputAllSpecificType enum. -func (e RegionsInfoV1OutputAllSpecificType) Valid() bool { - switch e { - case RegionsInfoV1OutputAllSpecificTypeSpecific: - return true - default: - return false - } -} - -// Defines values for RegionsInfoV1OutputRecommendationsSmartGroupCode. -const ( - RegionsInfoV1OutputRecommendationsSmartGroupCodeAmericas RegionsInfoV1OutputRecommendationsSmartGroupCode = "americas" - RegionsInfoV1OutputRecommendationsSmartGroupCodeApac RegionsInfoV1OutputRecommendationsSmartGroupCode = "apac" - RegionsInfoV1OutputRecommendationsSmartGroupCodeEmea RegionsInfoV1OutputRecommendationsSmartGroupCode = "emea" -) - -// Valid indicates whether the value is a known member of the RegionsInfoV1OutputRecommendationsSmartGroupCode enum. -func (e RegionsInfoV1OutputRecommendationsSmartGroupCode) Valid() bool { - switch e { - case RegionsInfoV1OutputRecommendationsSmartGroupCodeAmericas: - return true - case RegionsInfoV1OutputRecommendationsSmartGroupCodeApac: - return true - case RegionsInfoV1OutputRecommendationsSmartGroupCodeEmea: - return true - default: - return false - } -} - -// Defines values for RegionsInfoV1OutputRecommendationsSmartGroupType. -const ( - RegionsInfoV1OutputRecommendationsSmartGroupTypeSmartGroup RegionsInfoV1OutputRecommendationsSmartGroupType = "smartGroup" -) - -// Valid indicates whether the value is a known member of the RegionsInfoV1OutputRecommendationsSmartGroupType enum. -func (e RegionsInfoV1OutputRecommendationsSmartGroupType) Valid() bool { - switch e { - case RegionsInfoV1OutputRecommendationsSmartGroupTypeSmartGroup: - return true - default: - return false - } -} - -// Defines values for RegionsInfoV1OutputRecommendationsSpecificCode. -const ( - RegionsInfoV1OutputRecommendationsSpecificCodeApEast1 RegionsInfoV1OutputRecommendationsSpecificCode = "ap-east-1" - RegionsInfoV1OutputRecommendationsSpecificCodeApNortheast1 RegionsInfoV1OutputRecommendationsSpecificCode = "ap-northeast-1" - RegionsInfoV1OutputRecommendationsSpecificCodeApNortheast2 RegionsInfoV1OutputRecommendationsSpecificCode = "ap-northeast-2" - RegionsInfoV1OutputRecommendationsSpecificCodeApSouth1 RegionsInfoV1OutputRecommendationsSpecificCode = "ap-south-1" - RegionsInfoV1OutputRecommendationsSpecificCodeApSoutheast1 RegionsInfoV1OutputRecommendationsSpecificCode = "ap-southeast-1" - RegionsInfoV1OutputRecommendationsSpecificCodeApSoutheast2 RegionsInfoV1OutputRecommendationsSpecificCode = "ap-southeast-2" - RegionsInfoV1OutputRecommendationsSpecificCodeCaCentral1 RegionsInfoV1OutputRecommendationsSpecificCode = "ca-central-1" - RegionsInfoV1OutputRecommendationsSpecificCodeEuCentral1 RegionsInfoV1OutputRecommendationsSpecificCode = "eu-central-1" - RegionsInfoV1OutputRecommendationsSpecificCodeEuCentral2 RegionsInfoV1OutputRecommendationsSpecificCode = "eu-central-2" - RegionsInfoV1OutputRecommendationsSpecificCodeEuNorth1 RegionsInfoV1OutputRecommendationsSpecificCode = "eu-north-1" - RegionsInfoV1OutputRecommendationsSpecificCodeEuWest1 RegionsInfoV1OutputRecommendationsSpecificCode = "eu-west-1" - RegionsInfoV1OutputRecommendationsSpecificCodeEuWest2 RegionsInfoV1OutputRecommendationsSpecificCode = "eu-west-2" - RegionsInfoV1OutputRecommendationsSpecificCodeEuWest3 RegionsInfoV1OutputRecommendationsSpecificCode = "eu-west-3" - RegionsInfoV1OutputRecommendationsSpecificCodeSaEast1 RegionsInfoV1OutputRecommendationsSpecificCode = "sa-east-1" - RegionsInfoV1OutputRecommendationsSpecificCodeUsEast1 RegionsInfoV1OutputRecommendationsSpecificCode = "us-east-1" - RegionsInfoV1OutputRecommendationsSpecificCodeUsEast2 RegionsInfoV1OutputRecommendationsSpecificCode = "us-east-2" - RegionsInfoV1OutputRecommendationsSpecificCodeUsWest1 RegionsInfoV1OutputRecommendationsSpecificCode = "us-west-1" - RegionsInfoV1OutputRecommendationsSpecificCodeUsWest2 RegionsInfoV1OutputRecommendationsSpecificCode = "us-west-2" -) - -// Valid indicates whether the value is a known member of the RegionsInfoV1OutputRecommendationsSpecificCode enum. -func (e RegionsInfoV1OutputRecommendationsSpecificCode) Valid() bool { - switch e { - case RegionsInfoV1OutputRecommendationsSpecificCodeApEast1: - return true - case RegionsInfoV1OutputRecommendationsSpecificCodeApNortheast1: - return true - case RegionsInfoV1OutputRecommendationsSpecificCodeApNortheast2: - return true - case RegionsInfoV1OutputRecommendationsSpecificCodeApSouth1: - return true - case RegionsInfoV1OutputRecommendationsSpecificCodeApSoutheast1: - return true - case RegionsInfoV1OutputRecommendationsSpecificCodeApSoutheast2: - return true - case RegionsInfoV1OutputRecommendationsSpecificCodeCaCentral1: - return true - case RegionsInfoV1OutputRecommendationsSpecificCodeEuCentral1: - return true - case RegionsInfoV1OutputRecommendationsSpecificCodeEuCentral2: - return true - case RegionsInfoV1OutputRecommendationsSpecificCodeEuNorth1: - return true - case RegionsInfoV1OutputRecommendationsSpecificCodeEuWest1: - return true - case RegionsInfoV1OutputRecommendationsSpecificCodeEuWest2: - return true - case RegionsInfoV1OutputRecommendationsSpecificCodeEuWest3: - return true - case RegionsInfoV1OutputRecommendationsSpecificCodeSaEast1: - return true - case RegionsInfoV1OutputRecommendationsSpecificCodeUsEast1: - return true - case RegionsInfoV1OutputRecommendationsSpecificCodeUsEast2: - return true - case RegionsInfoV1OutputRecommendationsSpecificCodeUsWest1: - return true - case RegionsInfoV1OutputRecommendationsSpecificCodeUsWest2: - return true - default: - return false - } -} - -// Defines values for RegionsInfoV1OutputRecommendationsSpecificProvider. -const ( - RegionsInfoV1OutputRecommendationsSpecificProviderAWS RegionsInfoV1OutputRecommendationsSpecificProvider = "AWS" - RegionsInfoV1OutputRecommendationsSpecificProviderAWSK8S RegionsInfoV1OutputRecommendationsSpecificProvider = "AWS_K8S" - RegionsInfoV1OutputRecommendationsSpecificProviderAWSNIMBUS RegionsInfoV1OutputRecommendationsSpecificProvider = "AWS_NIMBUS" -) - -// Valid indicates whether the value is a known member of the RegionsInfoV1OutputRecommendationsSpecificProvider enum. -func (e RegionsInfoV1OutputRecommendationsSpecificProvider) Valid() bool { - switch e { - case RegionsInfoV1OutputRecommendationsSpecificProviderAWS: - return true - case RegionsInfoV1OutputRecommendationsSpecificProviderAWSK8S: - return true - case RegionsInfoV1OutputRecommendationsSpecificProviderAWSNIMBUS: - return true - default: - return false - } -} - -// Defines values for RegionsInfoV1OutputRecommendationsSpecificStatus. -const ( - RegionsInfoV1OutputRecommendationsSpecificStatusCapacity RegionsInfoV1OutputRecommendationsSpecificStatus = "capacity" - RegionsInfoV1OutputRecommendationsSpecificStatusOther RegionsInfoV1OutputRecommendationsSpecificStatus = "other" -) - -// Valid indicates whether the value is a known member of the RegionsInfoV1OutputRecommendationsSpecificStatus enum. -func (e RegionsInfoV1OutputRecommendationsSpecificStatus) Valid() bool { - switch e { - case RegionsInfoV1OutputRecommendationsSpecificStatusCapacity: - return true - case RegionsInfoV1OutputRecommendationsSpecificStatusOther: - return true - default: - return false - } -} - -// Defines values for RegionsInfoV1OutputRecommendationsSpecificType. -const ( - RegionsInfoV1OutputRecommendationsSpecificTypeSpecific RegionsInfoV1OutputRecommendationsSpecificType = "specific" -) - -// Valid indicates whether the value is a known member of the RegionsInfoV1OutputRecommendationsSpecificType enum. -func (e RegionsInfoV1OutputRecommendationsSpecificType) Valid() bool { - switch e { - case RegionsInfoV1OutputRecommendationsSpecificTypeSpecific: - return true - default: - return false - } -} - -// Defines values for SetUpReadReplicaBodyReadReplicaRegion. -const ( - SetUpReadReplicaBodyReadReplicaRegionApEast1 SetUpReadReplicaBodyReadReplicaRegion = "ap-east-1" - SetUpReadReplicaBodyReadReplicaRegionApNortheast1 SetUpReadReplicaBodyReadReplicaRegion = "ap-northeast-1" - SetUpReadReplicaBodyReadReplicaRegionApNortheast2 SetUpReadReplicaBodyReadReplicaRegion = "ap-northeast-2" - SetUpReadReplicaBodyReadReplicaRegionApSouth1 SetUpReadReplicaBodyReadReplicaRegion = "ap-south-1" - SetUpReadReplicaBodyReadReplicaRegionApSoutheast1 SetUpReadReplicaBodyReadReplicaRegion = "ap-southeast-1" - SetUpReadReplicaBodyReadReplicaRegionApSoutheast2 SetUpReadReplicaBodyReadReplicaRegion = "ap-southeast-2" - SetUpReadReplicaBodyReadReplicaRegionCaCentral1 SetUpReadReplicaBodyReadReplicaRegion = "ca-central-1" - SetUpReadReplicaBodyReadReplicaRegionEuCentral1 SetUpReadReplicaBodyReadReplicaRegion = "eu-central-1" - SetUpReadReplicaBodyReadReplicaRegionEuCentral2 SetUpReadReplicaBodyReadReplicaRegion = "eu-central-2" - SetUpReadReplicaBodyReadReplicaRegionEuNorth1 SetUpReadReplicaBodyReadReplicaRegion = "eu-north-1" - SetUpReadReplicaBodyReadReplicaRegionEuWest1 SetUpReadReplicaBodyReadReplicaRegion = "eu-west-1" - SetUpReadReplicaBodyReadReplicaRegionEuWest2 SetUpReadReplicaBodyReadReplicaRegion = "eu-west-2" - SetUpReadReplicaBodyReadReplicaRegionEuWest3 SetUpReadReplicaBodyReadReplicaRegion = "eu-west-3" - SetUpReadReplicaBodyReadReplicaRegionSaEast1 SetUpReadReplicaBodyReadReplicaRegion = "sa-east-1" - SetUpReadReplicaBodyReadReplicaRegionUsEast1 SetUpReadReplicaBodyReadReplicaRegion = "us-east-1" - SetUpReadReplicaBodyReadReplicaRegionUsEast2 SetUpReadReplicaBodyReadReplicaRegion = "us-east-2" - SetUpReadReplicaBodyReadReplicaRegionUsWest1 SetUpReadReplicaBodyReadReplicaRegion = "us-west-1" - SetUpReadReplicaBodyReadReplicaRegionUsWest2 SetUpReadReplicaBodyReadReplicaRegion = "us-west-2" -) - -// Valid indicates whether the value is a known member of the SetUpReadReplicaBodyReadReplicaRegion enum. -func (e SetUpReadReplicaBodyReadReplicaRegion) Valid() bool { - switch e { - case SetUpReadReplicaBodyReadReplicaRegionApEast1: - return true - case SetUpReadReplicaBodyReadReplicaRegionApNortheast1: - return true - case SetUpReadReplicaBodyReadReplicaRegionApNortheast2: - return true - case SetUpReadReplicaBodyReadReplicaRegionApSouth1: - return true - case SetUpReadReplicaBodyReadReplicaRegionApSoutheast1: - return true - case SetUpReadReplicaBodyReadReplicaRegionApSoutheast2: - return true - case SetUpReadReplicaBodyReadReplicaRegionCaCentral1: - return true - case SetUpReadReplicaBodyReadReplicaRegionEuCentral1: - return true - case SetUpReadReplicaBodyReadReplicaRegionEuCentral2: - return true - case SetUpReadReplicaBodyReadReplicaRegionEuNorth1: - return true - case SetUpReadReplicaBodyReadReplicaRegionEuWest1: - return true - case SetUpReadReplicaBodyReadReplicaRegionEuWest2: - return true - case SetUpReadReplicaBodyReadReplicaRegionEuWest3: - return true - case SetUpReadReplicaBodyReadReplicaRegionSaEast1: - return true - case SetUpReadReplicaBodyReadReplicaRegionUsEast1: - return true - case SetUpReadReplicaBodyReadReplicaRegionUsEast2: - return true - case SetUpReadReplicaBodyReadReplicaRegionUsWest1: - return true - case SetUpReadReplicaBodyReadReplicaRegionUsWest2: - return true - default: - return false - } -} - -// Defines values for SigningKeyResponseOutputAlgorithm. -const ( - SigningKeyResponseOutputAlgorithmES256 SigningKeyResponseOutputAlgorithm = "ES256" - SigningKeyResponseOutputAlgorithmEdDSA SigningKeyResponseOutputAlgorithm = "EdDSA" - SigningKeyResponseOutputAlgorithmHS256 SigningKeyResponseOutputAlgorithm = "HS256" - SigningKeyResponseOutputAlgorithmRS256 SigningKeyResponseOutputAlgorithm = "RS256" -) - -// Valid indicates whether the value is a known member of the SigningKeyResponseOutputAlgorithm enum. -func (e SigningKeyResponseOutputAlgorithm) Valid() bool { - switch e { - case SigningKeyResponseOutputAlgorithmES256: - return true - case SigningKeyResponseOutputAlgorithmEdDSA: - return true - case SigningKeyResponseOutputAlgorithmHS256: - return true - case SigningKeyResponseOutputAlgorithmRS256: - return true - default: - return false - } -} - -// Defines values for SigningKeyResponseOutputStatus. -const ( - SigningKeyResponseOutputStatusInUse SigningKeyResponseOutputStatus = "in_use" - SigningKeyResponseOutputStatusPreviouslyUsed SigningKeyResponseOutputStatus = "previously_used" - SigningKeyResponseOutputStatusRevoked SigningKeyResponseOutputStatus = "revoked" - SigningKeyResponseOutputStatusStandby SigningKeyResponseOutputStatus = "standby" -) - -// Valid indicates whether the value is a known member of the SigningKeyResponseOutputStatus enum. -func (e SigningKeyResponseOutputStatus) Valid() bool { - switch e { - case SigningKeyResponseOutputStatusInUse: - return true - case SigningKeyResponseOutputStatusPreviouslyUsed: - return true - case SigningKeyResponseOutputStatusRevoked: - return true - case SigningKeyResponseOutputStatusStandby: - return true - default: - return false - } -} - -// Defines values for SigningKeysResponseOutputKeysAlgorithm. -const ( - SigningKeysResponseOutputKeysAlgorithmES256 SigningKeysResponseOutputKeysAlgorithm = "ES256" - SigningKeysResponseOutputKeysAlgorithmEdDSA SigningKeysResponseOutputKeysAlgorithm = "EdDSA" - SigningKeysResponseOutputKeysAlgorithmHS256 SigningKeysResponseOutputKeysAlgorithm = "HS256" - SigningKeysResponseOutputKeysAlgorithmRS256 SigningKeysResponseOutputKeysAlgorithm = "RS256" -) - -// Valid indicates whether the value is a known member of the SigningKeysResponseOutputKeysAlgorithm enum. -func (e SigningKeysResponseOutputKeysAlgorithm) Valid() bool { - switch e { - case SigningKeysResponseOutputKeysAlgorithmES256: - return true - case SigningKeysResponseOutputKeysAlgorithmEdDSA: - return true - case SigningKeysResponseOutputKeysAlgorithmHS256: - return true - case SigningKeysResponseOutputKeysAlgorithmRS256: - return true - default: - return false - } -} - -// Defines values for SigningKeysResponseOutputKeysStatus. -const ( - SigningKeysResponseOutputKeysStatusInUse SigningKeysResponseOutputKeysStatus = "in_use" - SigningKeysResponseOutputKeysStatusPreviouslyUsed SigningKeysResponseOutputKeysStatus = "previously_used" - SigningKeysResponseOutputKeysStatusRevoked SigningKeysResponseOutputKeysStatus = "revoked" - SigningKeysResponseOutputKeysStatusStandby SigningKeysResponseOutputKeysStatus = "standby" -) - -// Valid indicates whether the value is a known member of the SigningKeysResponseOutputKeysStatus enum. -func (e SigningKeysResponseOutputKeysStatus) Valid() bool { - switch e { - case SigningKeysResponseOutputKeysStatusInUse: - return true - case SigningKeysResponseOutputKeysStatusPreviouslyUsed: - return true - case SigningKeysResponseOutputKeysStatusRevoked: - return true - case SigningKeysResponseOutputKeysStatusStandby: - return true - default: - return false - } -} - -// Defines values for SnippetListOutputDataType. -const ( - SnippetListOutputDataTypeSql SnippetListOutputDataType = "sql" -) - -// Valid indicates whether the value is a known member of the SnippetListOutputDataType enum. -func (e SnippetListOutputDataType) Valid() bool { - switch e { - case SnippetListOutputDataTypeSql: - return true - default: - return false - } -} - -// Defines values for SnippetListOutputDataVisibility. -const ( - SnippetListOutputDataVisibilityOrg SnippetListOutputDataVisibility = "org" - SnippetListOutputDataVisibilityProject SnippetListOutputDataVisibility = "project" - SnippetListOutputDataVisibilityPublic SnippetListOutputDataVisibility = "public" - SnippetListOutputDataVisibilityUser SnippetListOutputDataVisibility = "user" -) - -// Valid indicates whether the value is a known member of the SnippetListOutputDataVisibility enum. -func (e SnippetListOutputDataVisibility) Valid() bool { - switch e { - case SnippetListOutputDataVisibilityOrg: - return true - case SnippetListOutputDataVisibilityProject: - return true - case SnippetListOutputDataVisibilityPublic: - return true - case SnippetListOutputDataVisibilityUser: - return true - default: - return false - } -} - -// Defines values for SnippetResponseOutputType. -const ( - SnippetResponseOutputTypeSql SnippetResponseOutputType = "sql" -) - -// Valid indicates whether the value is a known member of the SnippetResponseOutputType enum. -func (e SnippetResponseOutputType) Valid() bool { - switch e { - case SnippetResponseOutputTypeSql: - return true - default: - return false - } -} - -// Defines values for SnippetResponseOutputVisibility. -const ( - SnippetResponseOutputVisibilityOrg SnippetResponseOutputVisibility = "org" - SnippetResponseOutputVisibilityProject SnippetResponseOutputVisibility = "project" - SnippetResponseOutputVisibilityPublic SnippetResponseOutputVisibility = "public" - SnippetResponseOutputVisibilityUser SnippetResponseOutputVisibility = "user" -) - -// Valid indicates whether the value is a known member of the SnippetResponseOutputVisibility enum. -func (e SnippetResponseOutputVisibility) Valid() bool { - switch e { - case SnippetResponseOutputVisibilityOrg: - return true - case SnippetResponseOutputVisibilityProject: - return true - case SnippetResponseOutputVisibilityPublic: - return true - case SnippetResponseOutputVisibilityUser: - return true - default: - return false - } -} - -// Defines values for StorageConfigResponseOutputExternalUpstreamTarget. -const ( - StorageConfigResponseOutputExternalUpstreamTargetCanary StorageConfigResponseOutputExternalUpstreamTarget = "canary" - StorageConfigResponseOutputExternalUpstreamTargetMain StorageConfigResponseOutputExternalUpstreamTarget = "main" -) - -// Valid indicates whether the value is a known member of the StorageConfigResponseOutputExternalUpstreamTarget enum. -func (e StorageConfigResponseOutputExternalUpstreamTarget) Valid() bool { - switch e { - case StorageConfigResponseOutputExternalUpstreamTargetCanary: - return true - case StorageConfigResponseOutputExternalUpstreamTargetMain: - return true - default: - return false - } -} - -// Defines values for SupavisorConfigResponseOutputDatabaseType. -const ( - SupavisorConfigResponseOutputDatabaseTypePRIMARY SupavisorConfigResponseOutputDatabaseType = "PRIMARY" - SupavisorConfigResponseOutputDatabaseTypeREADREPLICA SupavisorConfigResponseOutputDatabaseType = "READ_REPLICA" -) - -// Valid indicates whether the value is a known member of the SupavisorConfigResponseOutputDatabaseType enum. -func (e SupavisorConfigResponseOutputDatabaseType) Valid() bool { - switch e { - case SupavisorConfigResponseOutputDatabaseTypePRIMARY: - return true - case SupavisorConfigResponseOutputDatabaseTypeREADREPLICA: - return true - default: - return false - } -} - -// Defines values for SupavisorConfigResponseOutputPoolMode. -const ( - SupavisorConfigResponseOutputPoolModeSession SupavisorConfigResponseOutputPoolMode = "session" - SupavisorConfigResponseOutputPoolModeTransaction SupavisorConfigResponseOutputPoolMode = "transaction" -) - -// Valid indicates whether the value is a known member of the SupavisorConfigResponseOutputPoolMode enum. -func (e SupavisorConfigResponseOutputPoolMode) Valid() bool { - switch e { - case SupavisorConfigResponseOutputPoolModeSession: - return true - case SupavisorConfigResponseOutputPoolModeTransaction: - return true - default: - return false - } -} - -// Defines values for UpdateAuthConfigBodyDbMaxPoolSizeUnit. -const ( - UpdateAuthConfigBodyDbMaxPoolSizeUnitConnections UpdateAuthConfigBodyDbMaxPoolSizeUnit = "connections" - UpdateAuthConfigBodyDbMaxPoolSizeUnitLessThannil UpdateAuthConfigBodyDbMaxPoolSizeUnit = "" - UpdateAuthConfigBodyDbMaxPoolSizeUnitPercent UpdateAuthConfigBodyDbMaxPoolSizeUnit = "percent" -) - -// Valid indicates whether the value is a known member of the UpdateAuthConfigBodyDbMaxPoolSizeUnit enum. -func (e UpdateAuthConfigBodyDbMaxPoolSizeUnit) Valid() bool { - switch e { - case UpdateAuthConfigBodyDbMaxPoolSizeUnitConnections: - return true - case UpdateAuthConfigBodyDbMaxPoolSizeUnitLessThannil: - return true - case UpdateAuthConfigBodyDbMaxPoolSizeUnitPercent: - return true - default: - return false - } -} - -// Defines values for UpdateAuthConfigBodyPasswordRequiredCharacters. -const ( - UpdateAuthConfigBodyPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 UpdateAuthConfigBodyPasswordRequiredCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:0123456789" - UpdateAuthConfigBodyPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567891 UpdateAuthConfigBodyPasswordRequiredCharacters = "abcdefghijklmnopqrstuvwxyz:ABCDEFGHIJKLMNOPQRSTUVWXYZ:0123456789" - UpdateAuthConfigBodyPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567892 UpdateAuthConfigBodyPasswordRequiredCharacters = "abcdefghijklmnopqrstuvwxyz:ABCDEFGHIJKLMNOPQRSTUVWXYZ:0123456789:!@#$%^&*()_+-=[]{};'\\\\:\"|<>?,./`~" - UpdateAuthConfigBodyPasswordRequiredCharactersEmpty UpdateAuthConfigBodyPasswordRequiredCharacters = "" -) - -// Valid indicates whether the value is a known member of the UpdateAuthConfigBodyPasswordRequiredCharacters enum. -func (e UpdateAuthConfigBodyPasswordRequiredCharacters) Valid() bool { - switch e { - case UpdateAuthConfigBodyPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789: - return true - case UpdateAuthConfigBodyPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567891: - return true - case UpdateAuthConfigBodyPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567892: - return true - case UpdateAuthConfigBodyPasswordRequiredCharactersEmpty: - return true - default: - return false - } -} - -// Defines values for UpdateAuthConfigBodySecurityCaptchaProvider. -const ( - UpdateAuthConfigBodySecurityCaptchaProviderHcaptcha UpdateAuthConfigBodySecurityCaptchaProvider = "hcaptcha" - UpdateAuthConfigBodySecurityCaptchaProviderLessThannil UpdateAuthConfigBodySecurityCaptchaProvider = "" - UpdateAuthConfigBodySecurityCaptchaProviderTurnstile UpdateAuthConfigBodySecurityCaptchaProvider = "turnstile" -) - -// Valid indicates whether the value is a known member of the UpdateAuthConfigBodySecurityCaptchaProvider enum. -func (e UpdateAuthConfigBodySecurityCaptchaProvider) Valid() bool { - switch e { - case UpdateAuthConfigBodySecurityCaptchaProviderHcaptcha: - return true - case UpdateAuthConfigBodySecurityCaptchaProviderLessThannil: - return true - case UpdateAuthConfigBodySecurityCaptchaProviderTurnstile: - return true - default: - return false - } -} - -// Defines values for UpdateAuthConfigBodySmsProvider. -const ( - LessThannil UpdateAuthConfigBodySmsProvider = "" - Messagebird UpdateAuthConfigBodySmsProvider = "messagebird" - Textlocal UpdateAuthConfigBodySmsProvider = "textlocal" - Twilio UpdateAuthConfigBodySmsProvider = "twilio" - TwilioVerify UpdateAuthConfigBodySmsProvider = "twilio_verify" - Vonage UpdateAuthConfigBodySmsProvider = "vonage" -) - -// Valid indicates whether the value is a known member of the UpdateAuthConfigBodySmsProvider enum. -func (e UpdateAuthConfigBodySmsProvider) Valid() bool { - switch e { - case LessThannil: - return true - case Messagebird: - return true - case Textlocal: - return true - case Twilio: - return true - case TwilioVerify: - return true - case Vonage: - return true - default: - return false - } -} - -// Defines values for UpdateBranchBodyStatus. -const ( - UpdateBranchBodyStatusCREATINGPROJECT UpdateBranchBodyStatus = "CREATING_PROJECT" - UpdateBranchBodyStatusFUNCTIONSDEPLOYED UpdateBranchBodyStatus = "FUNCTIONS_DEPLOYED" - UpdateBranchBodyStatusFUNCTIONSFAILED UpdateBranchBodyStatus = "FUNCTIONS_FAILED" - UpdateBranchBodyStatusMIGRATIONSFAILED UpdateBranchBodyStatus = "MIGRATIONS_FAILED" - UpdateBranchBodyStatusMIGRATIONSPASSED UpdateBranchBodyStatus = "MIGRATIONS_PASSED" - UpdateBranchBodyStatusRUNNINGMIGRATIONS UpdateBranchBodyStatus = "RUNNING_MIGRATIONS" -) - -// Valid indicates whether the value is a known member of the UpdateBranchBodyStatus enum. -func (e UpdateBranchBodyStatus) Valid() bool { - switch e { - case UpdateBranchBodyStatusCREATINGPROJECT: - return true - case UpdateBranchBodyStatusFUNCTIONSDEPLOYED: - return true - case UpdateBranchBodyStatusFUNCTIONSFAILED: - return true - case UpdateBranchBodyStatusMIGRATIONSFAILED: - return true - case UpdateBranchBodyStatusMIGRATIONSPASSED: - return true - case UpdateBranchBodyStatusRUNNINGMIGRATIONS: - return true - default: - return false - } -} - -// Defines values for UpdateCustomHostnameResponseOutputStatus. -const ( - N1NotStarted UpdateCustomHostnameResponseOutputStatus = "1_not_started" - N2Initiated UpdateCustomHostnameResponseOutputStatus = "2_initiated" - N3ChallengeVerified UpdateCustomHostnameResponseOutputStatus = "3_challenge_verified" - N4OriginSetupCompleted UpdateCustomHostnameResponseOutputStatus = "4_origin_setup_completed" - N5ServicesReconfigured UpdateCustomHostnameResponseOutputStatus = "5_services_reconfigured" -) - -// Valid indicates whether the value is a known member of the UpdateCustomHostnameResponseOutputStatus enum. -func (e UpdateCustomHostnameResponseOutputStatus) Valid() bool { - switch e { - case N1NotStarted: - return true - case N2Initiated: - return true - case N3ChallengeVerified: - return true - case N4OriginSetupCompleted: - return true - case N5ServicesReconfigured: - return true - default: - return false - } -} - -// Defines values for UpdatePostgresConfigBodySessionReplicationRole. -const ( - UpdatePostgresConfigBodySessionReplicationRoleLocal UpdatePostgresConfigBodySessionReplicationRole = "local" - UpdatePostgresConfigBodySessionReplicationRoleOrigin UpdatePostgresConfigBodySessionReplicationRole = "origin" - UpdatePostgresConfigBodySessionReplicationRoleReplica UpdatePostgresConfigBodySessionReplicationRole = "replica" -) - -// Valid indicates whether the value is a known member of the UpdatePostgresConfigBodySessionReplicationRole enum. -func (e UpdatePostgresConfigBodySessionReplicationRole) Valid() bool { - switch e { - case UpdatePostgresConfigBodySessionReplicationRoleLocal: - return true - case UpdatePostgresConfigBodySessionReplicationRoleOrigin: - return true - case UpdatePostgresConfigBodySessionReplicationRoleReplica: - return true - default: - return false - } -} - -// Defines values for UpdateProviderBodyNameIdFormat. -const ( - UpdateProviderBodyNameIdFormatUrnOasisNamesTcSAML11NameidFormatEmailAddress UpdateProviderBodyNameIdFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" - UpdateProviderBodyNameIdFormatUrnOasisNamesTcSAML11NameidFormatUnspecified UpdateProviderBodyNameIdFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" - UpdateProviderBodyNameIdFormatUrnOasisNamesTcSAML20NameidFormatPersistent UpdateProviderBodyNameIdFormat = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" - UpdateProviderBodyNameIdFormatUrnOasisNamesTcSAML20NameidFormatTransient UpdateProviderBodyNameIdFormat = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" -) - -// Valid indicates whether the value is a known member of the UpdateProviderBodyNameIdFormat enum. -func (e UpdateProviderBodyNameIdFormat) Valid() bool { - switch e { - case UpdateProviderBodyNameIdFormatUrnOasisNamesTcSAML11NameidFormatEmailAddress: - return true - case UpdateProviderBodyNameIdFormatUrnOasisNamesTcSAML11NameidFormatUnspecified: - return true - case UpdateProviderBodyNameIdFormatUrnOasisNamesTcSAML20NameidFormatPersistent: - return true - case UpdateProviderBodyNameIdFormatUrnOasisNamesTcSAML20NameidFormatTransient: - return true - default: - return false - } -} - -// Defines values for UpdateRunStatusBodyClone. -const ( - UpdateRunStatusBodyCloneCREATED UpdateRunStatusBodyClone = "CREATED" - UpdateRunStatusBodyCloneDEAD UpdateRunStatusBodyClone = "DEAD" - UpdateRunStatusBodyCloneEXITED UpdateRunStatusBodyClone = "EXITED" - UpdateRunStatusBodyClonePAUSED UpdateRunStatusBodyClone = "PAUSED" - UpdateRunStatusBodyCloneREMOVING UpdateRunStatusBodyClone = "REMOVING" - UpdateRunStatusBodyCloneRESTARTING UpdateRunStatusBodyClone = "RESTARTING" - UpdateRunStatusBodyCloneRUNNING UpdateRunStatusBodyClone = "RUNNING" -) - -// Valid indicates whether the value is a known member of the UpdateRunStatusBodyClone enum. -func (e UpdateRunStatusBodyClone) Valid() bool { - switch e { - case UpdateRunStatusBodyCloneCREATED: - return true - case UpdateRunStatusBodyCloneDEAD: - return true - case UpdateRunStatusBodyCloneEXITED: - return true - case UpdateRunStatusBodyClonePAUSED: - return true - case UpdateRunStatusBodyCloneREMOVING: - return true - case UpdateRunStatusBodyCloneRESTARTING: - return true - case UpdateRunStatusBodyCloneRUNNING: - return true - default: - return false - } -} - -// Defines values for UpdateRunStatusBodyConfigure. -const ( - UpdateRunStatusBodyConfigureCREATED UpdateRunStatusBodyConfigure = "CREATED" - UpdateRunStatusBodyConfigureDEAD UpdateRunStatusBodyConfigure = "DEAD" - UpdateRunStatusBodyConfigureEXITED UpdateRunStatusBodyConfigure = "EXITED" - UpdateRunStatusBodyConfigurePAUSED UpdateRunStatusBodyConfigure = "PAUSED" - UpdateRunStatusBodyConfigureREMOVING UpdateRunStatusBodyConfigure = "REMOVING" - UpdateRunStatusBodyConfigureRESTARTING UpdateRunStatusBodyConfigure = "RESTARTING" - UpdateRunStatusBodyConfigureRUNNING UpdateRunStatusBodyConfigure = "RUNNING" -) - -// Valid indicates whether the value is a known member of the UpdateRunStatusBodyConfigure enum. -func (e UpdateRunStatusBodyConfigure) Valid() bool { - switch e { - case UpdateRunStatusBodyConfigureCREATED: - return true - case UpdateRunStatusBodyConfigureDEAD: - return true - case UpdateRunStatusBodyConfigureEXITED: - return true - case UpdateRunStatusBodyConfigurePAUSED: - return true - case UpdateRunStatusBodyConfigureREMOVING: - return true - case UpdateRunStatusBodyConfigureRESTARTING: - return true - case UpdateRunStatusBodyConfigureRUNNING: - return true - default: - return false - } -} - -// Defines values for UpdateRunStatusBodyDeploy. -const ( - UpdateRunStatusBodyDeployCREATED UpdateRunStatusBodyDeploy = "CREATED" - UpdateRunStatusBodyDeployDEAD UpdateRunStatusBodyDeploy = "DEAD" - UpdateRunStatusBodyDeployEXITED UpdateRunStatusBodyDeploy = "EXITED" - UpdateRunStatusBodyDeployPAUSED UpdateRunStatusBodyDeploy = "PAUSED" - UpdateRunStatusBodyDeployREMOVING UpdateRunStatusBodyDeploy = "REMOVING" - UpdateRunStatusBodyDeployRESTARTING UpdateRunStatusBodyDeploy = "RESTARTING" - UpdateRunStatusBodyDeployRUNNING UpdateRunStatusBodyDeploy = "RUNNING" -) - -// Valid indicates whether the value is a known member of the UpdateRunStatusBodyDeploy enum. -func (e UpdateRunStatusBodyDeploy) Valid() bool { - switch e { - case UpdateRunStatusBodyDeployCREATED: - return true - case UpdateRunStatusBodyDeployDEAD: - return true - case UpdateRunStatusBodyDeployEXITED: - return true - case UpdateRunStatusBodyDeployPAUSED: - return true - case UpdateRunStatusBodyDeployREMOVING: - return true - case UpdateRunStatusBodyDeployRESTARTING: - return true - case UpdateRunStatusBodyDeployRUNNING: - return true - default: - return false - } -} - -// Defines values for UpdateRunStatusBodyHealth. -const ( - UpdateRunStatusBodyHealthCREATED UpdateRunStatusBodyHealth = "CREATED" - UpdateRunStatusBodyHealthDEAD UpdateRunStatusBodyHealth = "DEAD" - UpdateRunStatusBodyHealthEXITED UpdateRunStatusBodyHealth = "EXITED" - UpdateRunStatusBodyHealthPAUSED UpdateRunStatusBodyHealth = "PAUSED" - UpdateRunStatusBodyHealthREMOVING UpdateRunStatusBodyHealth = "REMOVING" - UpdateRunStatusBodyHealthRESTARTING UpdateRunStatusBodyHealth = "RESTARTING" - UpdateRunStatusBodyHealthRUNNING UpdateRunStatusBodyHealth = "RUNNING" -) - -// Valid indicates whether the value is a known member of the UpdateRunStatusBodyHealth enum. -func (e UpdateRunStatusBodyHealth) Valid() bool { - switch e { - case UpdateRunStatusBodyHealthCREATED: - return true - case UpdateRunStatusBodyHealthDEAD: - return true - case UpdateRunStatusBodyHealthEXITED: - return true - case UpdateRunStatusBodyHealthPAUSED: - return true - case UpdateRunStatusBodyHealthREMOVING: - return true - case UpdateRunStatusBodyHealthRESTARTING: - return true - case UpdateRunStatusBodyHealthRUNNING: - return true - default: - return false - } -} - -// Defines values for UpdateRunStatusBodyMigrate. -const ( - UpdateRunStatusBodyMigrateCREATED UpdateRunStatusBodyMigrate = "CREATED" - UpdateRunStatusBodyMigrateDEAD UpdateRunStatusBodyMigrate = "DEAD" - UpdateRunStatusBodyMigrateEXITED UpdateRunStatusBodyMigrate = "EXITED" - UpdateRunStatusBodyMigratePAUSED UpdateRunStatusBodyMigrate = "PAUSED" - UpdateRunStatusBodyMigrateREMOVING UpdateRunStatusBodyMigrate = "REMOVING" - UpdateRunStatusBodyMigrateRESTARTING UpdateRunStatusBodyMigrate = "RESTARTING" - UpdateRunStatusBodyMigrateRUNNING UpdateRunStatusBodyMigrate = "RUNNING" -) - -// Valid indicates whether the value is a known member of the UpdateRunStatusBodyMigrate enum. -func (e UpdateRunStatusBodyMigrate) Valid() bool { - switch e { - case UpdateRunStatusBodyMigrateCREATED: - return true - case UpdateRunStatusBodyMigrateDEAD: - return true - case UpdateRunStatusBodyMigrateEXITED: - return true - case UpdateRunStatusBodyMigratePAUSED: - return true - case UpdateRunStatusBodyMigrateREMOVING: - return true - case UpdateRunStatusBodyMigrateRESTARTING: - return true - case UpdateRunStatusBodyMigrateRUNNING: - return true - default: - return false - } -} - -// Defines values for UpdateRunStatusBodyPull. -const ( - UpdateRunStatusBodyPullCREATED UpdateRunStatusBodyPull = "CREATED" - UpdateRunStatusBodyPullDEAD UpdateRunStatusBodyPull = "DEAD" - UpdateRunStatusBodyPullEXITED UpdateRunStatusBodyPull = "EXITED" - UpdateRunStatusBodyPullPAUSED UpdateRunStatusBodyPull = "PAUSED" - UpdateRunStatusBodyPullREMOVING UpdateRunStatusBodyPull = "REMOVING" - UpdateRunStatusBodyPullRESTARTING UpdateRunStatusBodyPull = "RESTARTING" - UpdateRunStatusBodyPullRUNNING UpdateRunStatusBodyPull = "RUNNING" -) - -// Valid indicates whether the value is a known member of the UpdateRunStatusBodyPull enum. -func (e UpdateRunStatusBodyPull) Valid() bool { - switch e { - case UpdateRunStatusBodyPullCREATED: - return true - case UpdateRunStatusBodyPullDEAD: - return true - case UpdateRunStatusBodyPullEXITED: - return true - case UpdateRunStatusBodyPullPAUSED: - return true - case UpdateRunStatusBodyPullREMOVING: - return true - case UpdateRunStatusBodyPullRESTARTING: - return true - case UpdateRunStatusBodyPullRUNNING: - return true - default: - return false - } -} - -// Defines values for UpdateRunStatusBodySeed. -const ( - UpdateRunStatusBodySeedCREATED UpdateRunStatusBodySeed = "CREATED" - UpdateRunStatusBodySeedDEAD UpdateRunStatusBodySeed = "DEAD" - UpdateRunStatusBodySeedEXITED UpdateRunStatusBodySeed = "EXITED" - UpdateRunStatusBodySeedPAUSED UpdateRunStatusBodySeed = "PAUSED" - UpdateRunStatusBodySeedREMOVING UpdateRunStatusBodySeed = "REMOVING" - UpdateRunStatusBodySeedRESTARTING UpdateRunStatusBodySeed = "RESTARTING" - UpdateRunStatusBodySeedRUNNING UpdateRunStatusBodySeed = "RUNNING" -) - -// Valid indicates whether the value is a known member of the UpdateRunStatusBodySeed enum. -func (e UpdateRunStatusBodySeed) Valid() bool { - switch e { - case UpdateRunStatusBodySeedCREATED: - return true - case UpdateRunStatusBodySeedDEAD: - return true - case UpdateRunStatusBodySeedEXITED: - return true - case UpdateRunStatusBodySeedPAUSED: - return true - case UpdateRunStatusBodySeedREMOVING: - return true - case UpdateRunStatusBodySeedRESTARTING: - return true - case UpdateRunStatusBodySeedRUNNING: - return true - default: - return false - } -} - -// Defines values for UpdateRunStatusResponseOutputMessage. -const ( - UpdateRunStatusResponseOutputMessageOk UpdateRunStatusResponseOutputMessage = "ok" -) - -// Valid indicates whether the value is a known member of the UpdateRunStatusResponseOutputMessage enum. -func (e UpdateRunStatusResponseOutputMessage) Valid() bool { - switch e { - case UpdateRunStatusResponseOutputMessageOk: - return true - default: - return false - } -} - -// Defines values for UpdateSigningKeyBodyStatus. -const ( - UpdateSigningKeyBodyStatusInUse UpdateSigningKeyBodyStatus = "in_use" - UpdateSigningKeyBodyStatusPreviouslyUsed UpdateSigningKeyBodyStatus = "previously_used" - UpdateSigningKeyBodyStatusRevoked UpdateSigningKeyBodyStatus = "revoked" - UpdateSigningKeyBodyStatusStandby UpdateSigningKeyBodyStatus = "standby" -) - -// Valid indicates whether the value is a known member of the UpdateSigningKeyBodyStatus enum. -func (e UpdateSigningKeyBodyStatus) Valid() bool { - switch e { - case UpdateSigningKeyBodyStatusInUse: - return true - case UpdateSigningKeyBodyStatusPreviouslyUsed: - return true - case UpdateSigningKeyBodyStatusRevoked: - return true - case UpdateSigningKeyBodyStatusStandby: - return true - default: - return false - } -} - -// Defines values for UpdateStorageConfigBodyExternalUpstreamTarget. -const ( - UpdateStorageConfigBodyExternalUpstreamTargetCanary UpdateStorageConfigBodyExternalUpstreamTarget = "canary" - UpdateStorageConfigBodyExternalUpstreamTargetMain UpdateStorageConfigBodyExternalUpstreamTarget = "main" -) - -// Valid indicates whether the value is a known member of the UpdateStorageConfigBodyExternalUpstreamTarget enum. -func (e UpdateStorageConfigBodyExternalUpstreamTarget) Valid() bool { - switch e { - case UpdateStorageConfigBodyExternalUpstreamTargetCanary: - return true - case UpdateStorageConfigBodyExternalUpstreamTargetMain: - return true - default: - return false - } -} - -// Defines values for UpdateSupavisorConfigBodyPoolMode. -const ( - UpdateSupavisorConfigBodyPoolModeSession UpdateSupavisorConfigBodyPoolMode = "session" - UpdateSupavisorConfigBodyPoolModeTransaction UpdateSupavisorConfigBodyPoolMode = "transaction" -) - -// Valid indicates whether the value is a known member of the UpdateSupavisorConfigBodyPoolMode enum. -func (e UpdateSupavisorConfigBodyPoolMode) Valid() bool { - switch e { - case UpdateSupavisorConfigBodyPoolModeSession: - return true - case UpdateSupavisorConfigBodyPoolModeTransaction: - return true - default: - return false - } -} - -// Defines values for UpgradeDatabaseBodyReleaseChannel. -const ( - UpgradeDatabaseBodyReleaseChannelAlpha UpgradeDatabaseBodyReleaseChannel = "alpha" - UpgradeDatabaseBodyReleaseChannelBeta UpgradeDatabaseBodyReleaseChannel = "beta" - UpgradeDatabaseBodyReleaseChannelGa UpgradeDatabaseBodyReleaseChannel = "ga" - UpgradeDatabaseBodyReleaseChannelInternal UpgradeDatabaseBodyReleaseChannel = "internal" - UpgradeDatabaseBodyReleaseChannelPreview UpgradeDatabaseBodyReleaseChannel = "preview" - UpgradeDatabaseBodyReleaseChannelWithdrawn UpgradeDatabaseBodyReleaseChannel = "withdrawn" -) - -// Valid indicates whether the value is a known member of the UpgradeDatabaseBodyReleaseChannel enum. -func (e UpgradeDatabaseBodyReleaseChannel) Valid() bool { - switch e { - case UpgradeDatabaseBodyReleaseChannelAlpha: - return true - case UpgradeDatabaseBodyReleaseChannelBeta: - return true - case UpgradeDatabaseBodyReleaseChannelGa: - return true - case UpgradeDatabaseBodyReleaseChannelInternal: - return true - case UpgradeDatabaseBodyReleaseChannelPreview: - return true - case UpgradeDatabaseBodyReleaseChannelWithdrawn: - return true - default: - return false - } -} - -// Defines values for V1BackupsResponseOutputBackupsStatus. -const ( - V1BackupsResponseOutputBackupsStatusARCHIVED V1BackupsResponseOutputBackupsStatus = "ARCHIVED" - V1BackupsResponseOutputBackupsStatusCANCELLED V1BackupsResponseOutputBackupsStatus = "CANCELLED" - V1BackupsResponseOutputBackupsStatusCOMPLETED V1BackupsResponseOutputBackupsStatus = "COMPLETED" - V1BackupsResponseOutputBackupsStatusFAILED V1BackupsResponseOutputBackupsStatus = "FAILED" - V1BackupsResponseOutputBackupsStatusPENDING V1BackupsResponseOutputBackupsStatus = "PENDING" - V1BackupsResponseOutputBackupsStatusREMOVED V1BackupsResponseOutputBackupsStatus = "REMOVED" -) - -// Valid indicates whether the value is a known member of the V1BackupsResponseOutputBackupsStatus enum. -func (e V1BackupsResponseOutputBackupsStatus) Valid() bool { - switch e { - case V1BackupsResponseOutputBackupsStatusARCHIVED: - return true - case V1BackupsResponseOutputBackupsStatusCANCELLED: - return true - case V1BackupsResponseOutputBackupsStatusCOMPLETED: - return true - case V1BackupsResponseOutputBackupsStatusFAILED: - return true - case V1BackupsResponseOutputBackupsStatusPENDING: - return true - case V1BackupsResponseOutputBackupsStatusREMOVED: - return true - default: - return false - } -} - -// Defines values for V1CreateProjectBodyDesiredInstanceSize. -const ( - V1CreateProjectBodyDesiredInstanceSizeLarge V1CreateProjectBodyDesiredInstanceSize = "large" - V1CreateProjectBodyDesiredInstanceSizeMedium V1CreateProjectBodyDesiredInstanceSize = "medium" - V1CreateProjectBodyDesiredInstanceSizeMicro V1CreateProjectBodyDesiredInstanceSize = "micro" - V1CreateProjectBodyDesiredInstanceSizeN12xlarge V1CreateProjectBodyDesiredInstanceSize = "12xlarge" - V1CreateProjectBodyDesiredInstanceSizeN16xlarge V1CreateProjectBodyDesiredInstanceSize = "16xlarge" - V1CreateProjectBodyDesiredInstanceSizeN24xlarge V1CreateProjectBodyDesiredInstanceSize = "24xlarge" - V1CreateProjectBodyDesiredInstanceSizeN24xlargeHighMemory V1CreateProjectBodyDesiredInstanceSize = "24xlarge_high_memory" - V1CreateProjectBodyDesiredInstanceSizeN24xlargeOptimizedCpu V1CreateProjectBodyDesiredInstanceSize = "24xlarge_optimized_cpu" - V1CreateProjectBodyDesiredInstanceSizeN24xlargeOptimizedMemory V1CreateProjectBodyDesiredInstanceSize = "24xlarge_optimized_memory" - V1CreateProjectBodyDesiredInstanceSizeN2xlarge V1CreateProjectBodyDesiredInstanceSize = "2xlarge" - V1CreateProjectBodyDesiredInstanceSizeN48xlarge V1CreateProjectBodyDesiredInstanceSize = "48xlarge" - V1CreateProjectBodyDesiredInstanceSizeN48xlargeHighMemory V1CreateProjectBodyDesiredInstanceSize = "48xlarge_high_memory" - V1CreateProjectBodyDesiredInstanceSizeN48xlargeOptimizedCpu V1CreateProjectBodyDesiredInstanceSize = "48xlarge_optimized_cpu" - V1CreateProjectBodyDesiredInstanceSizeN48xlargeOptimizedMemory V1CreateProjectBodyDesiredInstanceSize = "48xlarge_optimized_memory" - V1CreateProjectBodyDesiredInstanceSizeN4xlarge V1CreateProjectBodyDesiredInstanceSize = "4xlarge" - V1CreateProjectBodyDesiredInstanceSizeN8xlarge V1CreateProjectBodyDesiredInstanceSize = "8xlarge" - V1CreateProjectBodyDesiredInstanceSizeNano V1CreateProjectBodyDesiredInstanceSize = "nano" - V1CreateProjectBodyDesiredInstanceSizeSmall V1CreateProjectBodyDesiredInstanceSize = "small" - V1CreateProjectBodyDesiredInstanceSizeXlarge V1CreateProjectBodyDesiredInstanceSize = "xlarge" -) - -// Valid indicates whether the value is a known member of the V1CreateProjectBodyDesiredInstanceSize enum. -func (e V1CreateProjectBodyDesiredInstanceSize) Valid() bool { - switch e { - case V1CreateProjectBodyDesiredInstanceSizeLarge: - return true - case V1CreateProjectBodyDesiredInstanceSizeMedium: - return true - case V1CreateProjectBodyDesiredInstanceSizeMicro: - return true - case V1CreateProjectBodyDesiredInstanceSizeN12xlarge: - return true - case V1CreateProjectBodyDesiredInstanceSizeN16xlarge: - return true - case V1CreateProjectBodyDesiredInstanceSizeN24xlarge: - return true - case V1CreateProjectBodyDesiredInstanceSizeN24xlargeHighMemory: - return true - case V1CreateProjectBodyDesiredInstanceSizeN24xlargeOptimizedCpu: - return true - case V1CreateProjectBodyDesiredInstanceSizeN24xlargeOptimizedMemory: - return true - case V1CreateProjectBodyDesiredInstanceSizeN2xlarge: - return true - case V1CreateProjectBodyDesiredInstanceSizeN48xlarge: - return true - case V1CreateProjectBodyDesiredInstanceSizeN48xlargeHighMemory: - return true - case V1CreateProjectBodyDesiredInstanceSizeN48xlargeOptimizedCpu: - return true - case V1CreateProjectBodyDesiredInstanceSizeN48xlargeOptimizedMemory: - return true - case V1CreateProjectBodyDesiredInstanceSizeN4xlarge: - return true - case V1CreateProjectBodyDesiredInstanceSizeN8xlarge: - return true - case V1CreateProjectBodyDesiredInstanceSizeNano: - return true - case V1CreateProjectBodyDesiredInstanceSizeSmall: - return true - case V1CreateProjectBodyDesiredInstanceSizeXlarge: - return true - default: - return false - } -} - -// Defines values for V1CreateProjectBodyPlan. -const ( - V1CreateProjectBodyPlanFree V1CreateProjectBodyPlan = "free" - V1CreateProjectBodyPlanPro V1CreateProjectBodyPlan = "pro" -) - -// Valid indicates whether the value is a known member of the V1CreateProjectBodyPlan enum. -func (e V1CreateProjectBodyPlan) Valid() bool { - switch e { - case V1CreateProjectBodyPlanFree: - return true - case V1CreateProjectBodyPlanPro: - return true - default: - return false - } -} - -// Defines values for V1CreateProjectBodyRegion. -const ( - V1CreateProjectBodyRegionApEast1 V1CreateProjectBodyRegion = "ap-east-1" - V1CreateProjectBodyRegionApNortheast1 V1CreateProjectBodyRegion = "ap-northeast-1" - V1CreateProjectBodyRegionApNortheast2 V1CreateProjectBodyRegion = "ap-northeast-2" - V1CreateProjectBodyRegionApSouth1 V1CreateProjectBodyRegion = "ap-south-1" - V1CreateProjectBodyRegionApSoutheast1 V1CreateProjectBodyRegion = "ap-southeast-1" - V1CreateProjectBodyRegionApSoutheast2 V1CreateProjectBodyRegion = "ap-southeast-2" - V1CreateProjectBodyRegionCaCentral1 V1CreateProjectBodyRegion = "ca-central-1" - V1CreateProjectBodyRegionEuCentral1 V1CreateProjectBodyRegion = "eu-central-1" - V1CreateProjectBodyRegionEuCentral2 V1CreateProjectBodyRegion = "eu-central-2" - V1CreateProjectBodyRegionEuNorth1 V1CreateProjectBodyRegion = "eu-north-1" - V1CreateProjectBodyRegionEuWest1 V1CreateProjectBodyRegion = "eu-west-1" - V1CreateProjectBodyRegionEuWest2 V1CreateProjectBodyRegion = "eu-west-2" - V1CreateProjectBodyRegionEuWest3 V1CreateProjectBodyRegion = "eu-west-3" - V1CreateProjectBodyRegionSaEast1 V1CreateProjectBodyRegion = "sa-east-1" - V1CreateProjectBodyRegionUsEast1 V1CreateProjectBodyRegion = "us-east-1" - V1CreateProjectBodyRegionUsEast2 V1CreateProjectBodyRegion = "us-east-2" - V1CreateProjectBodyRegionUsWest1 V1CreateProjectBodyRegion = "us-west-1" - V1CreateProjectBodyRegionUsWest2 V1CreateProjectBodyRegion = "us-west-2" -) - -// Valid indicates whether the value is a known member of the V1CreateProjectBodyRegion enum. -func (e V1CreateProjectBodyRegion) Valid() bool { - switch e { - case V1CreateProjectBodyRegionApEast1: - return true - case V1CreateProjectBodyRegionApNortheast1: - return true - case V1CreateProjectBodyRegionApNortheast2: - return true - case V1CreateProjectBodyRegionApSouth1: - return true - case V1CreateProjectBodyRegionApSoutheast1: - return true - case V1CreateProjectBodyRegionApSoutheast2: - return true - case V1CreateProjectBodyRegionCaCentral1: - return true - case V1CreateProjectBodyRegionEuCentral1: - return true - case V1CreateProjectBodyRegionEuCentral2: - return true - case V1CreateProjectBodyRegionEuNorth1: - return true - case V1CreateProjectBodyRegionEuWest1: - return true - case V1CreateProjectBodyRegionEuWest2: - return true - case V1CreateProjectBodyRegionEuWest3: - return true - case V1CreateProjectBodyRegionSaEast1: - return true - case V1CreateProjectBodyRegionUsEast1: - return true - case V1CreateProjectBodyRegionUsEast2: - return true - case V1CreateProjectBodyRegionUsWest1: - return true - case V1CreateProjectBodyRegionUsWest2: - return true - default: - return false - } -} - -// Defines values for V1CreateProjectBodyRegionSelection0Code. -const ( - ApEast1 V1CreateProjectBodyRegionSelection0Code = "ap-east-1" - ApNortheast1 V1CreateProjectBodyRegionSelection0Code = "ap-northeast-1" - ApNortheast2 V1CreateProjectBodyRegionSelection0Code = "ap-northeast-2" - ApSouth1 V1CreateProjectBodyRegionSelection0Code = "ap-south-1" - ApSoutheast1 V1CreateProjectBodyRegionSelection0Code = "ap-southeast-1" - ApSoutheast2 V1CreateProjectBodyRegionSelection0Code = "ap-southeast-2" - CaCentral1 V1CreateProjectBodyRegionSelection0Code = "ca-central-1" - EuCentral1 V1CreateProjectBodyRegionSelection0Code = "eu-central-1" - EuCentral2 V1CreateProjectBodyRegionSelection0Code = "eu-central-2" - EuNorth1 V1CreateProjectBodyRegionSelection0Code = "eu-north-1" - EuWest1 V1CreateProjectBodyRegionSelection0Code = "eu-west-1" - EuWest2 V1CreateProjectBodyRegionSelection0Code = "eu-west-2" - EuWest3 V1CreateProjectBodyRegionSelection0Code = "eu-west-3" - SaEast1 V1CreateProjectBodyRegionSelection0Code = "sa-east-1" - UsEast1 V1CreateProjectBodyRegionSelection0Code = "us-east-1" - UsEast2 V1CreateProjectBodyRegionSelection0Code = "us-east-2" - UsWest1 V1CreateProjectBodyRegionSelection0Code = "us-west-1" - UsWest2 V1CreateProjectBodyRegionSelection0Code = "us-west-2" -) - -// Valid indicates whether the value is a known member of the V1CreateProjectBodyRegionSelection0Code enum. -func (e V1CreateProjectBodyRegionSelection0Code) Valid() bool { - switch e { - case ApEast1: - return true - case ApNortheast1: - return true - case ApNortheast2: - return true - case ApSouth1: - return true - case ApSoutheast1: - return true - case ApSoutheast2: - return true - case CaCentral1: - return true - case EuCentral1: - return true - case EuCentral2: - return true - case EuNorth1: - return true - case EuWest1: - return true - case EuWest2: - return true - case EuWest3: - return true - case SaEast1: - return true - case UsEast1: - return true - case UsEast2: - return true - case UsWest1: - return true - case UsWest2: - return true - default: - return false - } -} - -// Defines values for V1CreateProjectBodyRegionSelection0Type. -const ( - Specific V1CreateProjectBodyRegionSelection0Type = "specific" -) - -// Valid indicates whether the value is a known member of the V1CreateProjectBodyRegionSelection0Type enum. -func (e V1CreateProjectBodyRegionSelection0Type) Valid() bool { - switch e { - case Specific: - return true - default: - return false - } -} - -// Defines values for V1CreateProjectBodyRegionSelection1Code. -const ( - Americas V1CreateProjectBodyRegionSelection1Code = "americas" - Apac V1CreateProjectBodyRegionSelection1Code = "apac" - Emea V1CreateProjectBodyRegionSelection1Code = "emea" -) - -// Valid indicates whether the value is a known member of the V1CreateProjectBodyRegionSelection1Code enum. -func (e V1CreateProjectBodyRegionSelection1Code) Valid() bool { - switch e { - case Americas: - return true - case Apac: - return true - case Emea: - return true - default: - return false - } -} - -// Defines values for V1CreateProjectBodyRegionSelection1Type. -const ( - SmartGroup V1CreateProjectBodyRegionSelection1Type = "smartGroup" -) - -// Valid indicates whether the value is a known member of the V1CreateProjectBodyRegionSelection1Type enum. -func (e V1CreateProjectBodyRegionSelection1Type) Valid() bool { - switch e { - case SmartGroup: - return true - default: - return false - } -} - -// Defines values for V1ListEntitlementsResponseOutputEntitlementsFeatureKey. -const ( - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyApiMembersInvitations V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "api.members.invitations" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyApiMembersRoles V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "api.members.roles" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAssistantAdvanceModel V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "assistant.advance_model" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuditLogDrains V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "audit_log_drains" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthAdvancedAuthSettings V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "auth.advanced_auth_settings" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthCustomJwtTemplate V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "auth.custom_jwt_template" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthCustomOauthMaxProviders V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "auth.custom_oauth.max_providers" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthHooks V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "auth.hooks" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthLeakedPasswordProtection V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "auth.leaked_password_protection" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthMfaEnhancedSecurity V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "auth.mfa_enhanced_security" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthMfaPhone V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "auth.mfa_phone" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthMfaWebAuthn V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "auth.mfa_web_authn" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthPasswordHibp V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "auth.password_hibp" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthPerformanceSettings V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "auth.performance_settings" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthPlatformSso V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "auth.platform.sso" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthSaml2 V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "auth.saml_2" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthUserSessions V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "auth.user_sessions" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyBackupRestoreToNewProject V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "backup.restore_to_new_project" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyBackupRetentionDays V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "backup.retention_days" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyBackupSchedule V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "backup.schedule" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyBranchingLimit V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "branching_limit" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyBranchingPersistent V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "branching_persistent" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyCustomDomain V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "custom_domain" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyDedicatedPooler V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "dedicated_pooler" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyFunctionMaxCount V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "function.max_count" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyFunctionSizeLimitMb V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "function.size_limit_mb" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyInstancesComputeUpdateAvailableSizes V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "instances.compute_update_available_sizes" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyInstancesDiskModifications V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "instances.disk_modifications" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyInstancesHighAvailability V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "instances.high_availability" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyInstancesOrioledb V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "instances.orioledb" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyInstancesReadReplicas V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "instances.read_replicas" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyIntegrationsGithubConnections V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "integrations.github_connections" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyIntegrationsGithubPushWebhooksLimit V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "integrations.github_push_webhooks_limit" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyIpv4 V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "ipv4" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyLogDrains V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "log_drains" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyLogRetentionDays V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "log.retention_days" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyObservabilityDashboardAdvancedMetrics V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "observability.dashboard_advanced_metrics" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyPitrAvailableVariants V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "pitr.available_variants" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyProjectCloning V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "project_cloning" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyProjectPausing V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "project_pausing" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyProjectRestoreAfterExpiry V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "project_restore_after_expiry" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyProjectScopedRoles V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "project_scoped_roles" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyRealtimeMaxBytesPerSecond V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "realtime.max_bytes_per_second" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyRealtimeMaxChannelsPerClient V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "realtime.max_channels_per_client" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyRealtimeMaxConcurrentUsers V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "realtime.max_concurrent_users" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyRealtimeMaxEventsPerSecond V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "realtime.max_events_per_second" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyRealtimeMaxJoinsPerSecond V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "realtime.max_joins_per_second" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyRealtimeMaxPayloadSizeInKb V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "realtime.max_payload_size_in_kb" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyRealtimeMaxPresenceEventsPerSecond V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "realtime.max_presence_events_per_second" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyReplicationEtl V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "replication.etl" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeySecurityAuditLogsDays V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "security.audit_logs_days" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeySecurityEnforceMfa V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "security.enforce_mfa" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeySecurityIso27001Certificate V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "security.iso27001_certificate" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeySecurityMemberRoles V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "security.member_roles" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeySecurityPrivateLink V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "security.private_link" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeySecurityQuestionnaire V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "security.questionnaire" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeySecuritySoc2Report V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "security.soc2_report" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyStorageIcebergCatalog V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "storage.iceberg_catalog" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyStorageImageTransformations V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "storage.image_transformations" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyStorageMaxFileSize V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "storage.max_file_size" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyStorageMaxFileSizeConfigurable V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "storage.max_file_size.configurable" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyStoragePurgeCache V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "storage.purge_cache" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyStorageVectorBuckets V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "storage.vector_buckets" - V1ListEntitlementsResponseOutputEntitlementsFeatureKeyVanitySubdomain V1ListEntitlementsResponseOutputEntitlementsFeatureKey = "vanity_subdomain" -) - -// Valid indicates whether the value is a known member of the V1ListEntitlementsResponseOutputEntitlementsFeatureKey enum. -func (e V1ListEntitlementsResponseOutputEntitlementsFeatureKey) Valid() bool { - switch e { - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyApiMembersInvitations: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyApiMembersRoles: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAssistantAdvanceModel: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuditLogDrains: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthAdvancedAuthSettings: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthCustomJwtTemplate: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthCustomOauthMaxProviders: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthHooks: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthLeakedPasswordProtection: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthMfaEnhancedSecurity: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthMfaPhone: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthMfaWebAuthn: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthPasswordHibp: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthPerformanceSettings: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthPlatformSso: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthSaml2: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyAuthUserSessions: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyBackupRestoreToNewProject: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyBackupRetentionDays: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyBackupSchedule: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyBranchingLimit: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyBranchingPersistent: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyCustomDomain: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyDedicatedPooler: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyFunctionMaxCount: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyFunctionSizeLimitMb: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyInstancesComputeUpdateAvailableSizes: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyInstancesDiskModifications: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyInstancesHighAvailability: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyInstancesOrioledb: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyInstancesReadReplicas: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyIntegrationsGithubConnections: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyIntegrationsGithubPushWebhooksLimit: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyIpv4: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyLogDrains: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyLogRetentionDays: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyObservabilityDashboardAdvancedMetrics: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyPitrAvailableVariants: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyProjectCloning: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyProjectPausing: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyProjectRestoreAfterExpiry: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyProjectScopedRoles: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyRealtimeMaxBytesPerSecond: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyRealtimeMaxChannelsPerClient: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyRealtimeMaxConcurrentUsers: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyRealtimeMaxEventsPerSecond: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyRealtimeMaxJoinsPerSecond: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyRealtimeMaxPayloadSizeInKb: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyRealtimeMaxPresenceEventsPerSecond: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyReplicationEtl: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeySecurityAuditLogsDays: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeySecurityEnforceMfa: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeySecurityIso27001Certificate: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeySecurityMemberRoles: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeySecurityPrivateLink: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeySecurityQuestionnaire: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeySecuritySoc2Report: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyStorageIcebergCatalog: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyStorageImageTransformations: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyStorageMaxFileSize: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyStorageMaxFileSizeConfigurable: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyStoragePurgeCache: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyStorageVectorBuckets: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureKeyVanitySubdomain: - return true - default: - return false - } -} - -// Defines values for V1ListEntitlementsResponseOutputEntitlementsFeatureType. -const ( - V1ListEntitlementsResponseOutputEntitlementsFeatureTypeBoolean V1ListEntitlementsResponseOutputEntitlementsFeatureType = "boolean" - V1ListEntitlementsResponseOutputEntitlementsFeatureTypeNumeric V1ListEntitlementsResponseOutputEntitlementsFeatureType = "numeric" - V1ListEntitlementsResponseOutputEntitlementsFeatureTypeSet V1ListEntitlementsResponseOutputEntitlementsFeatureType = "set" -) - -// Valid indicates whether the value is a known member of the V1ListEntitlementsResponseOutputEntitlementsFeatureType enum. -func (e V1ListEntitlementsResponseOutputEntitlementsFeatureType) Valid() bool { - switch e { - case V1ListEntitlementsResponseOutputEntitlementsFeatureTypeBoolean: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureTypeNumeric: - return true - case V1ListEntitlementsResponseOutputEntitlementsFeatureTypeSet: - return true - default: - return false - } -} - -// Defines values for V1ListEntitlementsResponseOutputEntitlementsType. -const ( - V1ListEntitlementsResponseOutputEntitlementsTypeBoolean V1ListEntitlementsResponseOutputEntitlementsType = "boolean" - V1ListEntitlementsResponseOutputEntitlementsTypeNumeric V1ListEntitlementsResponseOutputEntitlementsType = "numeric" - V1ListEntitlementsResponseOutputEntitlementsTypeSet V1ListEntitlementsResponseOutputEntitlementsType = "set" -) - -// Valid indicates whether the value is a known member of the V1ListEntitlementsResponseOutputEntitlementsType enum. -func (e V1ListEntitlementsResponseOutputEntitlementsType) Valid() bool { - switch e { - case V1ListEntitlementsResponseOutputEntitlementsTypeBoolean: - return true - case V1ListEntitlementsResponseOutputEntitlementsTypeNumeric: - return true - case V1ListEntitlementsResponseOutputEntitlementsTypeSet: - return true - default: - return false - } -} - -// Defines values for V1OrganizationSlugResponseOutputAllowedReleaseChannels. -const ( - V1OrganizationSlugResponseOutputAllowedReleaseChannelsAlpha V1OrganizationSlugResponseOutputAllowedReleaseChannels = "alpha" - V1OrganizationSlugResponseOutputAllowedReleaseChannelsBeta V1OrganizationSlugResponseOutputAllowedReleaseChannels = "beta" - V1OrganizationSlugResponseOutputAllowedReleaseChannelsGa V1OrganizationSlugResponseOutputAllowedReleaseChannels = "ga" - V1OrganizationSlugResponseOutputAllowedReleaseChannelsInternal V1OrganizationSlugResponseOutputAllowedReleaseChannels = "internal" - V1OrganizationSlugResponseOutputAllowedReleaseChannelsPreview V1OrganizationSlugResponseOutputAllowedReleaseChannels = "preview" - V1OrganizationSlugResponseOutputAllowedReleaseChannelsWithdrawn V1OrganizationSlugResponseOutputAllowedReleaseChannels = "withdrawn" -) - -// Valid indicates whether the value is a known member of the V1OrganizationSlugResponseOutputAllowedReleaseChannels enum. -func (e V1OrganizationSlugResponseOutputAllowedReleaseChannels) Valid() bool { - switch e { - case V1OrganizationSlugResponseOutputAllowedReleaseChannelsAlpha: - return true - case V1OrganizationSlugResponseOutputAllowedReleaseChannelsBeta: - return true - case V1OrganizationSlugResponseOutputAllowedReleaseChannelsGa: - return true - case V1OrganizationSlugResponseOutputAllowedReleaseChannelsInternal: - return true - case V1OrganizationSlugResponseOutputAllowedReleaseChannelsPreview: - return true - case V1OrganizationSlugResponseOutputAllowedReleaseChannelsWithdrawn: - return true - default: - return false - } -} - -// Defines values for V1OrganizationSlugResponseOutputPlan. -const ( - V1OrganizationSlugResponseOutputPlanEnterprise V1OrganizationSlugResponseOutputPlan = "enterprise" - V1OrganizationSlugResponseOutputPlanFree V1OrganizationSlugResponseOutputPlan = "free" - V1OrganizationSlugResponseOutputPlanPlatform V1OrganizationSlugResponseOutputPlan = "platform" - V1OrganizationSlugResponseOutputPlanPro V1OrganizationSlugResponseOutputPlan = "pro" - V1OrganizationSlugResponseOutputPlanTeam V1OrganizationSlugResponseOutputPlan = "team" -) - -// Valid indicates whether the value is a known member of the V1OrganizationSlugResponseOutputPlan enum. -func (e V1OrganizationSlugResponseOutputPlan) Valid() bool { - switch e { - case V1OrganizationSlugResponseOutputPlanEnterprise: - return true - case V1OrganizationSlugResponseOutputPlanFree: - return true - case V1OrganizationSlugResponseOutputPlanPlatform: - return true - case V1OrganizationSlugResponseOutputPlanPro: - return true - case V1OrganizationSlugResponseOutputPlanTeam: - return true - default: - return false - } -} - -// Defines values for V1PgbouncerConfigResponseOutputPoolMode. -const ( - Session V1PgbouncerConfigResponseOutputPoolMode = "session" - Statement V1PgbouncerConfigResponseOutputPoolMode = "statement" - Transaction V1PgbouncerConfigResponseOutputPoolMode = "transaction" -) - -// Valid indicates whether the value is a known member of the V1PgbouncerConfigResponseOutputPoolMode enum. -func (e V1PgbouncerConfigResponseOutputPoolMode) Valid() bool { - switch e { - case Session: - return true - case Statement: - return true - case Transaction: - return true - default: - return false - } -} - -// Defines values for V1ProjectAdvisorsResponseOutputLintsCategories. -const ( - HEALTH V1ProjectAdvisorsResponseOutputLintsCategories = "HEALTH" - PERFORMANCE V1ProjectAdvisorsResponseOutputLintsCategories = "PERFORMANCE" - SECURITY V1ProjectAdvisorsResponseOutputLintsCategories = "SECURITY" -) - -// Valid indicates whether the value is a known member of the V1ProjectAdvisorsResponseOutputLintsCategories enum. -func (e V1ProjectAdvisorsResponseOutputLintsCategories) Valid() bool { - switch e { - case HEALTH: - return true - case PERFORMANCE: - return true - case SECURITY: - return true - default: - return false - } -} - -// Defines values for V1ProjectAdvisorsResponseOutputLintsFacing. -const ( - EXTERNAL V1ProjectAdvisorsResponseOutputLintsFacing = "EXTERNAL" -) - -// Valid indicates whether the value is a known member of the V1ProjectAdvisorsResponseOutputLintsFacing enum. -func (e V1ProjectAdvisorsResponseOutputLintsFacing) Valid() bool { - switch e { - case EXTERNAL: - return true - default: - return false - } -} - -// Defines values for V1ProjectAdvisorsResponseOutputLintsLevel. -const ( - ERROR V1ProjectAdvisorsResponseOutputLintsLevel = "ERROR" - INFO V1ProjectAdvisorsResponseOutputLintsLevel = "INFO" - WARN V1ProjectAdvisorsResponseOutputLintsLevel = "WARN" -) - -// Valid indicates whether the value is a known member of the V1ProjectAdvisorsResponseOutputLintsLevel enum. -func (e V1ProjectAdvisorsResponseOutputLintsLevel) Valid() bool { - switch e { - case ERROR: - return true - case INFO: - return true - case WARN: - return true - default: - return false - } -} - -// Defines values for V1ProjectAdvisorsResponseOutputLintsMetadataType. -const ( - V1ProjectAdvisorsResponseOutputLintsMetadataTypeAuth V1ProjectAdvisorsResponseOutputLintsMetadataType = "auth" - V1ProjectAdvisorsResponseOutputLintsMetadataTypeCompliance V1ProjectAdvisorsResponseOutputLintsMetadataType = "compliance" - V1ProjectAdvisorsResponseOutputLintsMetadataTypeExtension V1ProjectAdvisorsResponseOutputLintsMetadataType = "extension" - V1ProjectAdvisorsResponseOutputLintsMetadataTypeForeignTable V1ProjectAdvisorsResponseOutputLintsMetadataType = "foreign table" - V1ProjectAdvisorsResponseOutputLintsMetadataTypeFunction V1ProjectAdvisorsResponseOutputLintsMetadataType = "function" - V1ProjectAdvisorsResponseOutputLintsMetadataTypeHealth V1ProjectAdvisorsResponseOutputLintsMetadataType = "health" - V1ProjectAdvisorsResponseOutputLintsMetadataTypeMaterializedView V1ProjectAdvisorsResponseOutputLintsMetadataType = "materialized view" - V1ProjectAdvisorsResponseOutputLintsMetadataTypeTable V1ProjectAdvisorsResponseOutputLintsMetadataType = "table" - V1ProjectAdvisorsResponseOutputLintsMetadataTypeView V1ProjectAdvisorsResponseOutputLintsMetadataType = "view" -) - -// Valid indicates whether the value is a known member of the V1ProjectAdvisorsResponseOutputLintsMetadataType enum. -func (e V1ProjectAdvisorsResponseOutputLintsMetadataType) Valid() bool { - switch e { - case V1ProjectAdvisorsResponseOutputLintsMetadataTypeAuth: - return true - case V1ProjectAdvisorsResponseOutputLintsMetadataTypeCompliance: - return true - case V1ProjectAdvisorsResponseOutputLintsMetadataTypeExtension: - return true - case V1ProjectAdvisorsResponseOutputLintsMetadataTypeForeignTable: - return true - case V1ProjectAdvisorsResponseOutputLintsMetadataTypeFunction: - return true - case V1ProjectAdvisorsResponseOutputLintsMetadataTypeHealth: - return true - case V1ProjectAdvisorsResponseOutputLintsMetadataTypeMaterializedView: - return true - case V1ProjectAdvisorsResponseOutputLintsMetadataTypeTable: - return true - case V1ProjectAdvisorsResponseOutputLintsMetadataTypeView: - return true - default: - return false - } -} - -// Defines values for V1ProjectAdvisorsResponseOutputLintsName. -const ( - AdvisorCheckUnavailable V1ProjectAdvisorsResponseOutputLintsName = "advisor_check_unavailable" - AuthInsufficientMfaOptions V1ProjectAdvisorsResponseOutputLintsName = "auth_insufficient_mfa_options" - AuthLeakedPasswordProtection V1ProjectAdvisorsResponseOutputLintsName = "auth_leaked_password_protection" - AuthOtpLongExpiry V1ProjectAdvisorsResponseOutputLintsName = "auth_otp_long_expiry" - AuthOtpShortLength V1ProjectAdvisorsResponseOutputLintsName = "auth_otp_short_length" - AuthPasswordPolicyMissing V1ProjectAdvisorsResponseOutputLintsName = "auth_password_policy_missing" - AuthRlsInitplan V1ProjectAdvisorsResponseOutputLintsName = "auth_rls_initplan" - AuthUsersExposed V1ProjectAdvisorsResponseOutputLintsName = "auth_users_exposed" - DbConnectionFailing V1ProjectAdvisorsResponseOutputLintsName = "db_connection_failing" - DbConnectionLimitReached V1ProjectAdvisorsResponseOutputLintsName = "db_connection_limit_reached" - DbNotReachable V1ProjectAdvisorsResponseOutputLintsName = "db_not_reachable" - DuplicateIndex V1ProjectAdvisorsResponseOutputLintsName = "duplicate_index" - ExtensionInPublic V1ProjectAdvisorsResponseOutputLintsName = "extension_in_public" - ForeignTableInApi V1ProjectAdvisorsResponseOutputLintsName = "foreign_table_in_api" - FunctionSearchPathMutable V1ProjectAdvisorsResponseOutputLintsName = "function_search_path_mutable" - InstanceAlertFiring V1ProjectAdvisorsResponseOutputLintsName = "instance_alert_firing" - InstanceDbDown V1ProjectAdvisorsResponseOutputLintsName = "instance_db_down" - InstanceTelemetryLost V1ProjectAdvisorsResponseOutputLintsName = "instance_telemetry_lost" - LeakedServiceKey V1ProjectAdvisorsResponseOutputLintsName = "leaked_service_key" - LogAuthErrorRateHigh V1ProjectAdvisorsResponseOutputLintsName = "log_auth_error_rate_high" - LogConnectionsNotEnabled V1ProjectAdvisorsResponseOutputLintsName = "log_connections_not_enabled" - LogDataApiErrorRateHigh V1ProjectAdvisorsResponseOutputLintsName = "log_data_api_error_rate_high" - LogEdgeFunctionErrorRateHigh V1ProjectAdvisorsResponseOutputLintsName = "log_edge_function_error_rate_high" - LogStorageErrorRateHigh V1ProjectAdvisorsResponseOutputLintsName = "log_storage_error_rate_high" - MaterializedViewInApi V1ProjectAdvisorsResponseOutputLintsName = "materialized_view_in_api" - MultiplePermissivePolicies V1ProjectAdvisorsResponseOutputLintsName = "multiple_permissive_policies" - NetworkRestrictionsNotSet V1ProjectAdvisorsResponseOutputLintsName = "network_restrictions_not_set" - NoBackupAdmin V1ProjectAdvisorsResponseOutputLintsName = "no_backup_admin" - NoPrimaryKey V1ProjectAdvisorsResponseOutputLintsName = "no_primary_key" - PasswordRequirementsMinLength V1ProjectAdvisorsResponseOutputLintsName = "password_requirements_min_length" - PitrNotEnabled V1ProjectAdvisorsResponseOutputLintsName = "pitr_not_enabled" - PolicyExistsRlsDisabled V1ProjectAdvisorsResponseOutputLintsName = "policy_exists_rls_disabled" - ProjectNotActive V1ProjectAdvisorsResponseOutputLintsName = "project_not_active" - RealtimeAuthorizationTimeouts V1ProjectAdvisorsResponseOutputLintsName = "realtime_authorization_timeouts" - RealtimeConcurrentUsersNearLimit V1ProjectAdvisorsResponseOutputLintsName = "realtime_concurrent_users_near_limit" - RealtimePostgresChangesSpike V1ProjectAdvisorsResponseOutputLintsName = "realtime_postgres_changes_spike" - RlsDisabledInPublic V1ProjectAdvisorsResponseOutputLintsName = "rls_disabled_in_public" - RlsEnabledNoPolicy V1ProjectAdvisorsResponseOutputLintsName = "rls_enabled_no_policy" - RlsReferencesUserMetadata V1ProjectAdvisorsResponseOutputLintsName = "rls_references_user_metadata" - SecurityDefinerView V1ProjectAdvisorsResponseOutputLintsName = "security_definer_view" - SslNotEnforced V1ProjectAdvisorsResponseOutputLintsName = "ssl_not_enforced" - UnindexedForeignKeys V1ProjectAdvisorsResponseOutputLintsName = "unindexed_foreign_keys" - UnsupportedRegTypes V1ProjectAdvisorsResponseOutputLintsName = "unsupported_reg_types" - UnusedIndex V1ProjectAdvisorsResponseOutputLintsName = "unused_index" - VulnerablePostgresVersion V1ProjectAdvisorsResponseOutputLintsName = "vulnerable_postgres_version" -) - -// Valid indicates whether the value is a known member of the V1ProjectAdvisorsResponseOutputLintsName enum. -func (e V1ProjectAdvisorsResponseOutputLintsName) Valid() bool { - switch e { - case AdvisorCheckUnavailable: - return true - case AuthInsufficientMfaOptions: - return true - case AuthLeakedPasswordProtection: - return true - case AuthOtpLongExpiry: - return true - case AuthOtpShortLength: - return true - case AuthPasswordPolicyMissing: - return true - case AuthRlsInitplan: - return true - case AuthUsersExposed: - return true - case DbConnectionFailing: - return true - case DbConnectionLimitReached: - return true - case DbNotReachable: - return true - case DuplicateIndex: - return true - case ExtensionInPublic: - return true - case ForeignTableInApi: - return true - case FunctionSearchPathMutable: - return true - case InstanceAlertFiring: - return true - case InstanceDbDown: - return true - case InstanceTelemetryLost: - return true - case LeakedServiceKey: - return true - case LogAuthErrorRateHigh: - return true - case LogConnectionsNotEnabled: - return true - case LogDataApiErrorRateHigh: - return true - case LogEdgeFunctionErrorRateHigh: - return true - case LogStorageErrorRateHigh: - return true - case MaterializedViewInApi: - return true - case MultiplePermissivePolicies: - return true - case NetworkRestrictionsNotSet: - return true - case NoBackupAdmin: - return true - case NoPrimaryKey: - return true - case PasswordRequirementsMinLength: - return true - case PitrNotEnabled: - return true - case PolicyExistsRlsDisabled: - return true - case ProjectNotActive: - return true - case RealtimeAuthorizationTimeouts: - return true - case RealtimeConcurrentUsersNearLimit: - return true - case RealtimePostgresChangesSpike: - return true - case RlsDisabledInPublic: - return true - case RlsEnabledNoPolicy: - return true - case RlsReferencesUserMetadata: - return true - case SecurityDefinerView: - return true - case SslNotEnforced: - return true - case UnindexedForeignKeys: - return true - case UnsupportedRegTypes: - return true - case UnusedIndex: - return true - case VulnerablePostgresVersion: - return true - default: - return false - } -} - -// Defines values for V1ProjectResponseOutputStatus. -const ( - V1ProjectResponseOutputStatusACTIVEHEALTHY V1ProjectResponseOutputStatus = "ACTIVE_HEALTHY" - V1ProjectResponseOutputStatusACTIVEUNHEALTHY V1ProjectResponseOutputStatus = "ACTIVE_UNHEALTHY" - V1ProjectResponseOutputStatusCOMINGUP V1ProjectResponseOutputStatus = "COMING_UP" - V1ProjectResponseOutputStatusGOINGDOWN V1ProjectResponseOutputStatus = "GOING_DOWN" - V1ProjectResponseOutputStatusINACTIVE V1ProjectResponseOutputStatus = "INACTIVE" - V1ProjectResponseOutputStatusINITFAILED V1ProjectResponseOutputStatus = "INIT_FAILED" - V1ProjectResponseOutputStatusPAUSEFAILED V1ProjectResponseOutputStatus = "PAUSE_FAILED" - V1ProjectResponseOutputStatusPAUSING V1ProjectResponseOutputStatus = "PAUSING" - V1ProjectResponseOutputStatusREMOVED V1ProjectResponseOutputStatus = "REMOVED" - V1ProjectResponseOutputStatusRESIZING V1ProjectResponseOutputStatus = "RESIZING" - V1ProjectResponseOutputStatusRESTARTING V1ProjectResponseOutputStatus = "RESTARTING" - V1ProjectResponseOutputStatusRESTOREFAILED V1ProjectResponseOutputStatus = "RESTORE_FAILED" - V1ProjectResponseOutputStatusRESTORING V1ProjectResponseOutputStatus = "RESTORING" - V1ProjectResponseOutputStatusUNKNOWN V1ProjectResponseOutputStatus = "UNKNOWN" - V1ProjectResponseOutputStatusUPGRADING V1ProjectResponseOutputStatus = "UPGRADING" -) - -// Valid indicates whether the value is a known member of the V1ProjectResponseOutputStatus enum. -func (e V1ProjectResponseOutputStatus) Valid() bool { - switch e { - case V1ProjectResponseOutputStatusACTIVEHEALTHY: - return true - case V1ProjectResponseOutputStatusACTIVEUNHEALTHY: - return true - case V1ProjectResponseOutputStatusCOMINGUP: - return true - case V1ProjectResponseOutputStatusGOINGDOWN: - return true - case V1ProjectResponseOutputStatusINACTIVE: - return true - case V1ProjectResponseOutputStatusINITFAILED: - return true - case V1ProjectResponseOutputStatusPAUSEFAILED: - return true - case V1ProjectResponseOutputStatusPAUSING: - return true - case V1ProjectResponseOutputStatusREMOVED: - return true - case V1ProjectResponseOutputStatusRESIZING: - return true - case V1ProjectResponseOutputStatusRESTARTING: - return true - case V1ProjectResponseOutputStatusRESTOREFAILED: - return true - case V1ProjectResponseOutputStatusRESTORING: - return true - case V1ProjectResponseOutputStatusUNKNOWN: - return true - case V1ProjectResponseOutputStatusUPGRADING: - return true - default: - return false - } -} - -// Defines values for V1ProjectWithDatabaseResponseOutputStatus. -const ( - V1ProjectWithDatabaseResponseOutputStatusACTIVEHEALTHY V1ProjectWithDatabaseResponseOutputStatus = "ACTIVE_HEALTHY" - V1ProjectWithDatabaseResponseOutputStatusACTIVEUNHEALTHY V1ProjectWithDatabaseResponseOutputStatus = "ACTIVE_UNHEALTHY" - V1ProjectWithDatabaseResponseOutputStatusCOMINGUP V1ProjectWithDatabaseResponseOutputStatus = "COMING_UP" - V1ProjectWithDatabaseResponseOutputStatusGOINGDOWN V1ProjectWithDatabaseResponseOutputStatus = "GOING_DOWN" - V1ProjectWithDatabaseResponseOutputStatusINACTIVE V1ProjectWithDatabaseResponseOutputStatus = "INACTIVE" - V1ProjectWithDatabaseResponseOutputStatusINITFAILED V1ProjectWithDatabaseResponseOutputStatus = "INIT_FAILED" - V1ProjectWithDatabaseResponseOutputStatusPAUSEFAILED V1ProjectWithDatabaseResponseOutputStatus = "PAUSE_FAILED" - V1ProjectWithDatabaseResponseOutputStatusPAUSING V1ProjectWithDatabaseResponseOutputStatus = "PAUSING" - V1ProjectWithDatabaseResponseOutputStatusREMOVED V1ProjectWithDatabaseResponseOutputStatus = "REMOVED" - V1ProjectWithDatabaseResponseOutputStatusRESIZING V1ProjectWithDatabaseResponseOutputStatus = "RESIZING" - V1ProjectWithDatabaseResponseOutputStatusRESTARTING V1ProjectWithDatabaseResponseOutputStatus = "RESTARTING" - V1ProjectWithDatabaseResponseOutputStatusRESTOREFAILED V1ProjectWithDatabaseResponseOutputStatus = "RESTORE_FAILED" - V1ProjectWithDatabaseResponseOutputStatusRESTORING V1ProjectWithDatabaseResponseOutputStatus = "RESTORING" - V1ProjectWithDatabaseResponseOutputStatusUNKNOWN V1ProjectWithDatabaseResponseOutputStatus = "UNKNOWN" - V1ProjectWithDatabaseResponseOutputStatusUPGRADING V1ProjectWithDatabaseResponseOutputStatus = "UPGRADING" -) - -// Valid indicates whether the value is a known member of the V1ProjectWithDatabaseResponseOutputStatus enum. -func (e V1ProjectWithDatabaseResponseOutputStatus) Valid() bool { - switch e { - case V1ProjectWithDatabaseResponseOutputStatusACTIVEHEALTHY: - return true - case V1ProjectWithDatabaseResponseOutputStatusACTIVEUNHEALTHY: - return true - case V1ProjectWithDatabaseResponseOutputStatusCOMINGUP: - return true - case V1ProjectWithDatabaseResponseOutputStatusGOINGDOWN: - return true - case V1ProjectWithDatabaseResponseOutputStatusINACTIVE: - return true - case V1ProjectWithDatabaseResponseOutputStatusINITFAILED: - return true - case V1ProjectWithDatabaseResponseOutputStatusPAUSEFAILED: - return true - case V1ProjectWithDatabaseResponseOutputStatusPAUSING: - return true - case V1ProjectWithDatabaseResponseOutputStatusREMOVED: - return true - case V1ProjectWithDatabaseResponseOutputStatusRESIZING: - return true - case V1ProjectWithDatabaseResponseOutputStatusRESTARTING: - return true - case V1ProjectWithDatabaseResponseOutputStatusRESTOREFAILED: - return true - case V1ProjectWithDatabaseResponseOutputStatusRESTORING: - return true - case V1ProjectWithDatabaseResponseOutputStatusUNKNOWN: - return true - case V1ProjectWithDatabaseResponseOutputStatusUPGRADING: - return true - default: - return false - } -} - -// Defines values for V1RestorePointResponseStatus. -const ( - V1RestorePointResponseStatusAVAILABLE V1RestorePointResponseStatus = "AVAILABLE" - V1RestorePointResponseStatusFAILED V1RestorePointResponseStatus = "FAILED" - V1RestorePointResponseStatusPENDING V1RestorePointResponseStatus = "PENDING" - V1RestorePointResponseStatusREMOVED V1RestorePointResponseStatus = "REMOVED" -) - -// Valid indicates whether the value is a known member of the V1RestorePointResponseStatus enum. -func (e V1RestorePointResponseStatus) Valid() bool { - switch e { - case V1RestorePointResponseStatusAVAILABLE: - return true - case V1RestorePointResponseStatusFAILED: - return true - case V1RestorePointResponseStatusPENDING: - return true - case V1RestorePointResponseStatusREMOVED: - return true - default: - return false - } -} - -// Defines values for V1ServiceHealthResponseOutputInfo0Name. -const ( - GoTrue V1ServiceHealthResponseOutputInfo0Name = "GoTrue" -) - -// Valid indicates whether the value is a known member of the V1ServiceHealthResponseOutputInfo0Name enum. -func (e V1ServiceHealthResponseOutputInfo0Name) Valid() bool { - switch e { - case GoTrue: - return true - default: - return false - } -} - -// Defines values for V1ServiceHealthResponseOutputName. -const ( - V1ServiceHealthResponseOutputNameAuth V1ServiceHealthResponseOutputName = "auth" - V1ServiceHealthResponseOutputNameDb V1ServiceHealthResponseOutputName = "db" - V1ServiceHealthResponseOutputNameDbPostgresUser V1ServiceHealthResponseOutputName = "db_postgres_user" - V1ServiceHealthResponseOutputNamePgBouncer V1ServiceHealthResponseOutputName = "pg_bouncer" - V1ServiceHealthResponseOutputNamePooler V1ServiceHealthResponseOutputName = "pooler" - V1ServiceHealthResponseOutputNameRealtime V1ServiceHealthResponseOutputName = "realtime" - V1ServiceHealthResponseOutputNameRest V1ServiceHealthResponseOutputName = "rest" - V1ServiceHealthResponseOutputNameStorage V1ServiceHealthResponseOutputName = "storage" -) - -// Valid indicates whether the value is a known member of the V1ServiceHealthResponseOutputName enum. -func (e V1ServiceHealthResponseOutputName) Valid() bool { - switch e { - case V1ServiceHealthResponseOutputNameAuth: - return true - case V1ServiceHealthResponseOutputNameDb: - return true - case V1ServiceHealthResponseOutputNameDbPostgresUser: - return true - case V1ServiceHealthResponseOutputNamePgBouncer: - return true - case V1ServiceHealthResponseOutputNamePooler: - return true - case V1ServiceHealthResponseOutputNameRealtime: - return true - case V1ServiceHealthResponseOutputNameRest: - return true - case V1ServiceHealthResponseOutputNameStorage: - return true - default: - return false - } -} - -// Defines values for V1ServiceHealthResponseOutputStatus. -const ( - ACTIVEHEALTHY V1ServiceHealthResponseOutputStatus = "ACTIVE_HEALTHY" - COMINGUP V1ServiceHealthResponseOutputStatus = "COMING_UP" - UNHEALTHY V1ServiceHealthResponseOutputStatus = "UNHEALTHY" -) - -// Valid indicates whether the value is a known member of the V1ServiceHealthResponseOutputStatus enum. -func (e V1ServiceHealthResponseOutputStatus) Valid() bool { - switch e { - case ACTIVEHEALTHY: - return true - case COMINGUP: - return true - case UNHEALTHY: - return true - default: - return false - } -} - -// Defines values for VanitySubdomainConfigResponseOutputStatus. -const ( - Active VanitySubdomainConfigResponseOutputStatus = "active" - CustomDomainUsed VanitySubdomainConfigResponseOutputStatus = "custom-domain-used" - NotUsed VanitySubdomainConfigResponseOutputStatus = "not-used" -) - -// Valid indicates whether the value is a known member of the VanitySubdomainConfigResponseOutputStatus enum. -func (e VanitySubdomainConfigResponseOutputStatus) Valid() bool { - switch e { - case Active: - return true - case CustomDomainUsed: - return true - case NotUsed: - return true - default: - return false - } -} - -// Defines values for V1AuthorizeUserParamsResponseType. -const ( - V1AuthorizeUserParamsResponseTypeCode V1AuthorizeUserParamsResponseType = "code" - V1AuthorizeUserParamsResponseTypeIdTokenToken V1AuthorizeUserParamsResponseType = "id_token token" - V1AuthorizeUserParamsResponseTypeToken V1AuthorizeUserParamsResponseType = "token" -) - -// Valid indicates whether the value is a known member of the V1AuthorizeUserParamsResponseType enum. -func (e V1AuthorizeUserParamsResponseType) Valid() bool { - switch e { - case V1AuthorizeUserParamsResponseTypeCode: - return true - case V1AuthorizeUserParamsResponseTypeIdTokenToken: - return true - case V1AuthorizeUserParamsResponseTypeToken: - return true - default: - return false - } -} - -// Defines values for V1AuthorizeUserParamsCodeChallengeMethod. -const ( - V1AuthorizeUserParamsCodeChallengeMethodPlain V1AuthorizeUserParamsCodeChallengeMethod = "plain" - V1AuthorizeUserParamsCodeChallengeMethodS256 V1AuthorizeUserParamsCodeChallengeMethod = "S256" - V1AuthorizeUserParamsCodeChallengeMethodSha256 V1AuthorizeUserParamsCodeChallengeMethod = "sha256" -) - -// Valid indicates whether the value is a known member of the V1AuthorizeUserParamsCodeChallengeMethod enum. -func (e V1AuthorizeUserParamsCodeChallengeMethod) Valid() bool { - switch e { - case V1AuthorizeUserParamsCodeChallengeMethodPlain: - return true - case V1AuthorizeUserParamsCodeChallengeMethodS256: - return true - case V1AuthorizeUserParamsCodeChallengeMethodSha256: - return true - default: - return false - } -} - -// Defines values for V1OauthAuthorizeProjectClaimParamsResponseType. -const ( - V1OauthAuthorizeProjectClaimParamsResponseTypeCode V1OauthAuthorizeProjectClaimParamsResponseType = "code" - V1OauthAuthorizeProjectClaimParamsResponseTypeIdTokenToken V1OauthAuthorizeProjectClaimParamsResponseType = "id_token token" - V1OauthAuthorizeProjectClaimParamsResponseTypeToken V1OauthAuthorizeProjectClaimParamsResponseType = "token" -) - -// Valid indicates whether the value is a known member of the V1OauthAuthorizeProjectClaimParamsResponseType enum. -func (e V1OauthAuthorizeProjectClaimParamsResponseType) Valid() bool { - switch e { - case V1OauthAuthorizeProjectClaimParamsResponseTypeCode: - return true - case V1OauthAuthorizeProjectClaimParamsResponseTypeIdTokenToken: - return true - case V1OauthAuthorizeProjectClaimParamsResponseTypeToken: - return true - default: - return false - } -} - -// Defines values for V1OauthAuthorizeProjectClaimParamsCodeChallengeMethod. -const ( - V1OauthAuthorizeProjectClaimParamsCodeChallengeMethodPlain V1OauthAuthorizeProjectClaimParamsCodeChallengeMethod = "plain" - V1OauthAuthorizeProjectClaimParamsCodeChallengeMethodS256 V1OauthAuthorizeProjectClaimParamsCodeChallengeMethod = "S256" - V1OauthAuthorizeProjectClaimParamsCodeChallengeMethodSha256 V1OauthAuthorizeProjectClaimParamsCodeChallengeMethod = "sha256" -) - -// Valid indicates whether the value is a known member of the V1OauthAuthorizeProjectClaimParamsCodeChallengeMethod enum. -func (e V1OauthAuthorizeProjectClaimParamsCodeChallengeMethod) Valid() bool { - switch e { - case V1OauthAuthorizeProjectClaimParamsCodeChallengeMethodPlain: - return true - case V1OauthAuthorizeProjectClaimParamsCodeChallengeMethodS256: - return true - case V1OauthAuthorizeProjectClaimParamsCodeChallengeMethodSha256: - return true - default: - return false - } -} - -// Defines values for V1GetAllProjectsForOrganizationParamsSort. -const ( - CreatedAsc V1GetAllProjectsForOrganizationParamsSort = "created_asc" - CreatedDesc V1GetAllProjectsForOrganizationParamsSort = "created_desc" - NameAsc V1GetAllProjectsForOrganizationParamsSort = "name_asc" - NameDesc V1GetAllProjectsForOrganizationParamsSort = "name_desc" -) - -// Valid indicates whether the value is a known member of the V1GetAllProjectsForOrganizationParamsSort enum. -func (e V1GetAllProjectsForOrganizationParamsSort) Valid() bool { - switch e { - case CreatedAsc: - return true - case CreatedDesc: - return true - case NameAsc: - return true - case NameDesc: - return true - default: - return false - } -} - -// Defines values for V1GetAvailableRegionsParamsContinent. -const ( - AF V1GetAvailableRegionsParamsContinent = "AF" - AN V1GetAvailableRegionsParamsContinent = "AN" - AS V1GetAvailableRegionsParamsContinent = "AS" - EU V1GetAvailableRegionsParamsContinent = "EU" - NA V1GetAvailableRegionsParamsContinent = "NA" - OC V1GetAvailableRegionsParamsContinent = "OC" - SA V1GetAvailableRegionsParamsContinent = "SA" -) - -// Valid indicates whether the value is a known member of the V1GetAvailableRegionsParamsContinent enum. -func (e V1GetAvailableRegionsParamsContinent) Valid() bool { - switch e { - case AF: - return true - case AN: - return true - case AS: - return true - case EU: - return true - case NA: - return true - case OC: - return true - case SA: - return true - default: - return false - } -} - -// Defines values for V1GetAvailableRegionsParamsDesiredInstanceSize. -const ( - V1GetAvailableRegionsParamsDesiredInstanceSizeLarge V1GetAvailableRegionsParamsDesiredInstanceSize = "large" - V1GetAvailableRegionsParamsDesiredInstanceSizeMedium V1GetAvailableRegionsParamsDesiredInstanceSize = "medium" - V1GetAvailableRegionsParamsDesiredInstanceSizeMicro V1GetAvailableRegionsParamsDesiredInstanceSize = "micro" - V1GetAvailableRegionsParamsDesiredInstanceSizeN12xlarge V1GetAvailableRegionsParamsDesiredInstanceSize = "12xlarge" - V1GetAvailableRegionsParamsDesiredInstanceSizeN16xlarge V1GetAvailableRegionsParamsDesiredInstanceSize = "16xlarge" - V1GetAvailableRegionsParamsDesiredInstanceSizeN24xlarge V1GetAvailableRegionsParamsDesiredInstanceSize = "24xlarge" - V1GetAvailableRegionsParamsDesiredInstanceSizeN24xlargeHighMemory V1GetAvailableRegionsParamsDesiredInstanceSize = "24xlarge_high_memory" - V1GetAvailableRegionsParamsDesiredInstanceSizeN24xlargeOptimizedCpu V1GetAvailableRegionsParamsDesiredInstanceSize = "24xlarge_optimized_cpu" - V1GetAvailableRegionsParamsDesiredInstanceSizeN24xlargeOptimizedMemory V1GetAvailableRegionsParamsDesiredInstanceSize = "24xlarge_optimized_memory" - V1GetAvailableRegionsParamsDesiredInstanceSizeN2xlarge V1GetAvailableRegionsParamsDesiredInstanceSize = "2xlarge" - V1GetAvailableRegionsParamsDesiredInstanceSizeN48xlarge V1GetAvailableRegionsParamsDesiredInstanceSize = "48xlarge" - V1GetAvailableRegionsParamsDesiredInstanceSizeN48xlargeHighMemory V1GetAvailableRegionsParamsDesiredInstanceSize = "48xlarge_high_memory" - V1GetAvailableRegionsParamsDesiredInstanceSizeN48xlargeOptimizedCpu V1GetAvailableRegionsParamsDesiredInstanceSize = "48xlarge_optimized_cpu" - V1GetAvailableRegionsParamsDesiredInstanceSizeN48xlargeOptimizedMemory V1GetAvailableRegionsParamsDesiredInstanceSize = "48xlarge_optimized_memory" - V1GetAvailableRegionsParamsDesiredInstanceSizeN4xlarge V1GetAvailableRegionsParamsDesiredInstanceSize = "4xlarge" - V1GetAvailableRegionsParamsDesiredInstanceSizeN8xlarge V1GetAvailableRegionsParamsDesiredInstanceSize = "8xlarge" - V1GetAvailableRegionsParamsDesiredInstanceSizeNano V1GetAvailableRegionsParamsDesiredInstanceSize = "nano" - V1GetAvailableRegionsParamsDesiredInstanceSizeSmall V1GetAvailableRegionsParamsDesiredInstanceSize = "small" - V1GetAvailableRegionsParamsDesiredInstanceSizeXlarge V1GetAvailableRegionsParamsDesiredInstanceSize = "xlarge" -) - -// Valid indicates whether the value is a known member of the V1GetAvailableRegionsParamsDesiredInstanceSize enum. -func (e V1GetAvailableRegionsParamsDesiredInstanceSize) Valid() bool { - switch e { - case V1GetAvailableRegionsParamsDesiredInstanceSizeLarge: - return true - case V1GetAvailableRegionsParamsDesiredInstanceSizeMedium: - return true - case V1GetAvailableRegionsParamsDesiredInstanceSizeMicro: - return true - case V1GetAvailableRegionsParamsDesiredInstanceSizeN12xlarge: - return true - case V1GetAvailableRegionsParamsDesiredInstanceSizeN16xlarge: - return true - case V1GetAvailableRegionsParamsDesiredInstanceSizeN24xlarge: - return true - case V1GetAvailableRegionsParamsDesiredInstanceSizeN24xlargeHighMemory: - return true - case V1GetAvailableRegionsParamsDesiredInstanceSizeN24xlargeOptimizedCpu: - return true - case V1GetAvailableRegionsParamsDesiredInstanceSizeN24xlargeOptimizedMemory: - return true - case V1GetAvailableRegionsParamsDesiredInstanceSizeN2xlarge: - return true - case V1GetAvailableRegionsParamsDesiredInstanceSizeN48xlarge: - return true - case V1GetAvailableRegionsParamsDesiredInstanceSizeN48xlargeHighMemory: - return true - case V1GetAvailableRegionsParamsDesiredInstanceSizeN48xlargeOptimizedCpu: - return true - case V1GetAvailableRegionsParamsDesiredInstanceSizeN48xlargeOptimizedMemory: - return true - case V1GetAvailableRegionsParamsDesiredInstanceSizeN4xlarge: - return true - case V1GetAvailableRegionsParamsDesiredInstanceSizeN8xlarge: - return true - case V1GetAvailableRegionsParamsDesiredInstanceSizeNano: - return true - case V1GetAvailableRegionsParamsDesiredInstanceSizeSmall: - return true - case V1GetAvailableRegionsParamsDesiredInstanceSizeXlarge: - return true - default: - return false - } -} - -// Defines values for V1GetSecurityAdvisorsParamsLintType. -const ( - Sql V1GetSecurityAdvisorsParamsLintType = "sql" -) - -// Valid indicates whether the value is a known member of the V1GetSecurityAdvisorsParamsLintType enum. -func (e V1GetSecurityAdvisorsParamsLintType) Valid() bool { - switch e { - case Sql: - return true - default: - return false - } -} - -// Defines values for V1GetProjectFunctionCombinedStatsParamsInterval. -const ( - V1GetProjectFunctionCombinedStatsParamsIntervalN15min V1GetProjectFunctionCombinedStatsParamsInterval = "15min" - V1GetProjectFunctionCombinedStatsParamsIntervalN1day V1GetProjectFunctionCombinedStatsParamsInterval = "1day" - V1GetProjectFunctionCombinedStatsParamsIntervalN1hr V1GetProjectFunctionCombinedStatsParamsInterval = "1hr" - V1GetProjectFunctionCombinedStatsParamsIntervalN3hr V1GetProjectFunctionCombinedStatsParamsInterval = "3hr" -) - -// Valid indicates whether the value is a known member of the V1GetProjectFunctionCombinedStatsParamsInterval enum. -func (e V1GetProjectFunctionCombinedStatsParamsInterval) Valid() bool { - switch e { - case V1GetProjectFunctionCombinedStatsParamsIntervalN15min: - return true - case V1GetProjectFunctionCombinedStatsParamsIntervalN1day: - return true - case V1GetProjectFunctionCombinedStatsParamsIntervalN1hr: - return true - case V1GetProjectFunctionCombinedStatsParamsIntervalN3hr: - return true - default: - return false - } -} - -// Defines values for V1GetProjectUsageApiCountParamsInterval. -const ( - V1GetProjectUsageApiCountParamsIntervalN15min V1GetProjectUsageApiCountParamsInterval = "15min" - V1GetProjectUsageApiCountParamsIntervalN1day V1GetProjectUsageApiCountParamsInterval = "1day" - V1GetProjectUsageApiCountParamsIntervalN1hr V1GetProjectUsageApiCountParamsInterval = "1hr" - V1GetProjectUsageApiCountParamsIntervalN30min V1GetProjectUsageApiCountParamsInterval = "30min" - V1GetProjectUsageApiCountParamsIntervalN3day V1GetProjectUsageApiCountParamsInterval = "3day" - V1GetProjectUsageApiCountParamsIntervalN3hr V1GetProjectUsageApiCountParamsInterval = "3hr" - V1GetProjectUsageApiCountParamsIntervalN7day V1GetProjectUsageApiCountParamsInterval = "7day" -) - -// Valid indicates whether the value is a known member of the V1GetProjectUsageApiCountParamsInterval enum. -func (e V1GetProjectUsageApiCountParamsInterval) Valid() bool { - switch e { - case V1GetProjectUsageApiCountParamsIntervalN15min: - return true - case V1GetProjectUsageApiCountParamsIntervalN1day: - return true - case V1GetProjectUsageApiCountParamsIntervalN1hr: - return true - case V1GetProjectUsageApiCountParamsIntervalN30min: - return true - case V1GetProjectUsageApiCountParamsIntervalN3day: - return true - case V1GetProjectUsageApiCountParamsIntervalN3hr: - return true - case V1GetProjectUsageApiCountParamsIntervalN7day: - return true - default: - return false - } -} - -// Defines values for V1RemoveProjectAddonParamsAddonVariant0. -const ( - V1RemoveProjectAddonParamsAddonVariant0Ci12xlarge V1RemoveProjectAddonParamsAddonVariant0 = "ci_12xlarge" - V1RemoveProjectAddonParamsAddonVariant0Ci16xlarge V1RemoveProjectAddonParamsAddonVariant0 = "ci_16xlarge" - V1RemoveProjectAddonParamsAddonVariant0Ci24xlarge V1RemoveProjectAddonParamsAddonVariant0 = "ci_24xlarge" - V1RemoveProjectAddonParamsAddonVariant0Ci24xlargeHighMemory V1RemoveProjectAddonParamsAddonVariant0 = "ci_24xlarge_high_memory" - V1RemoveProjectAddonParamsAddonVariant0Ci24xlargeOptimizedCpu V1RemoveProjectAddonParamsAddonVariant0 = "ci_24xlarge_optimized_cpu" - V1RemoveProjectAddonParamsAddonVariant0Ci24xlargeOptimizedMemory V1RemoveProjectAddonParamsAddonVariant0 = "ci_24xlarge_optimized_memory" - V1RemoveProjectAddonParamsAddonVariant0Ci2xlarge V1RemoveProjectAddonParamsAddonVariant0 = "ci_2xlarge" - V1RemoveProjectAddonParamsAddonVariant0Ci48xlarge V1RemoveProjectAddonParamsAddonVariant0 = "ci_48xlarge" - V1RemoveProjectAddonParamsAddonVariant0Ci48xlargeHighMemory V1RemoveProjectAddonParamsAddonVariant0 = "ci_48xlarge_high_memory" - V1RemoveProjectAddonParamsAddonVariant0Ci48xlargeOptimizedCpu V1RemoveProjectAddonParamsAddonVariant0 = "ci_48xlarge_optimized_cpu" - V1RemoveProjectAddonParamsAddonVariant0Ci48xlargeOptimizedMemory V1RemoveProjectAddonParamsAddonVariant0 = "ci_48xlarge_optimized_memory" - V1RemoveProjectAddonParamsAddonVariant0Ci4xlarge V1RemoveProjectAddonParamsAddonVariant0 = "ci_4xlarge" - V1RemoveProjectAddonParamsAddonVariant0Ci8xlarge V1RemoveProjectAddonParamsAddonVariant0 = "ci_8xlarge" - V1RemoveProjectAddonParamsAddonVariant0CiLarge V1RemoveProjectAddonParamsAddonVariant0 = "ci_large" - V1RemoveProjectAddonParamsAddonVariant0CiMedium V1RemoveProjectAddonParamsAddonVariant0 = "ci_medium" - V1RemoveProjectAddonParamsAddonVariant0CiMicro V1RemoveProjectAddonParamsAddonVariant0 = "ci_micro" - V1RemoveProjectAddonParamsAddonVariant0CiSmall V1RemoveProjectAddonParamsAddonVariant0 = "ci_small" - V1RemoveProjectAddonParamsAddonVariant0CiXlarge V1RemoveProjectAddonParamsAddonVariant0 = "ci_xlarge" -) - -// Valid indicates whether the value is a known member of the V1RemoveProjectAddonParamsAddonVariant0 enum. -func (e V1RemoveProjectAddonParamsAddonVariant0) Valid() bool { - switch e { - case V1RemoveProjectAddonParamsAddonVariant0Ci12xlarge: - return true - case V1RemoveProjectAddonParamsAddonVariant0Ci16xlarge: - return true - case V1RemoveProjectAddonParamsAddonVariant0Ci24xlarge: - return true - case V1RemoveProjectAddonParamsAddonVariant0Ci24xlargeHighMemory: - return true - case V1RemoveProjectAddonParamsAddonVariant0Ci24xlargeOptimizedCpu: - return true - case V1RemoveProjectAddonParamsAddonVariant0Ci24xlargeOptimizedMemory: - return true - case V1RemoveProjectAddonParamsAddonVariant0Ci2xlarge: - return true - case V1RemoveProjectAddonParamsAddonVariant0Ci48xlarge: - return true - case V1RemoveProjectAddonParamsAddonVariant0Ci48xlargeHighMemory: - return true - case V1RemoveProjectAddonParamsAddonVariant0Ci48xlargeOptimizedCpu: - return true - case V1RemoveProjectAddonParamsAddonVariant0Ci48xlargeOptimizedMemory: - return true - case V1RemoveProjectAddonParamsAddonVariant0Ci4xlarge: - return true - case V1RemoveProjectAddonParamsAddonVariant0Ci8xlarge: - return true - case V1RemoveProjectAddonParamsAddonVariant0CiLarge: - return true - case V1RemoveProjectAddonParamsAddonVariant0CiMedium: - return true - case V1RemoveProjectAddonParamsAddonVariant0CiMicro: - return true - case V1RemoveProjectAddonParamsAddonVariant0CiSmall: - return true - case V1RemoveProjectAddonParamsAddonVariant0CiXlarge: - return true - default: - return false - } -} - -// Defines values for V1RemoveProjectAddonParamsAddonVariant1. -const ( - V1RemoveProjectAddonParamsAddonVariant1CdDefault V1RemoveProjectAddonParamsAddonVariant1 = "cd_default" -) - -// Valid indicates whether the value is a known member of the V1RemoveProjectAddonParamsAddonVariant1 enum. -func (e V1RemoveProjectAddonParamsAddonVariant1) Valid() bool { - switch e { - case V1RemoveProjectAddonParamsAddonVariant1CdDefault: - return true - default: - return false - } -} - -// Defines values for V1RemoveProjectAddonParamsAddonVariant2. -const ( - V1RemoveProjectAddonParamsAddonVariant2Pitr14 V1RemoveProjectAddonParamsAddonVariant2 = "pitr_14" - V1RemoveProjectAddonParamsAddonVariant2Pitr28 V1RemoveProjectAddonParamsAddonVariant2 = "pitr_28" - V1RemoveProjectAddonParamsAddonVariant2Pitr7 V1RemoveProjectAddonParamsAddonVariant2 = "pitr_7" -) - -// Valid indicates whether the value is a known member of the V1RemoveProjectAddonParamsAddonVariant2 enum. -func (e V1RemoveProjectAddonParamsAddonVariant2) Valid() bool { - switch e { - case V1RemoveProjectAddonParamsAddonVariant2Pitr14: - return true - case V1RemoveProjectAddonParamsAddonVariant2Pitr28: - return true - case V1RemoveProjectAddonParamsAddonVariant2Pitr7: - return true - default: - return false - } -} - -// Defines values for V1RemoveProjectAddonParamsAddonVariant3. -const ( - V1RemoveProjectAddonParamsAddonVariant3Ipv4Default V1RemoveProjectAddonParamsAddonVariant3 = "ipv4_default" -) - -// Valid indicates whether the value is a known member of the V1RemoveProjectAddonParamsAddonVariant3 enum. -func (e V1RemoveProjectAddonParamsAddonVariant3) Valid() bool { - switch e { - case V1RemoveProjectAddonParamsAddonVariant3Ipv4Default: - return true - default: - return false - } -} - -// Defines values for V1GetServicesHealthParamsServices. -const ( - V1GetServicesHealthParamsServicesAuth V1GetServicesHealthParamsServices = "auth" - V1GetServicesHealthParamsServicesDb V1GetServicesHealthParamsServices = "db" - V1GetServicesHealthParamsServicesDbPostgresUser V1GetServicesHealthParamsServices = "db_postgres_user" - V1GetServicesHealthParamsServicesPgBouncer V1GetServicesHealthParamsServices = "pg_bouncer" - V1GetServicesHealthParamsServicesPooler V1GetServicesHealthParamsServices = "pooler" - V1GetServicesHealthParamsServicesRealtime V1GetServicesHealthParamsServices = "realtime" - V1GetServicesHealthParamsServicesRest V1GetServicesHealthParamsServices = "rest" - V1GetServicesHealthParamsServicesStorage V1GetServicesHealthParamsServices = "storage" -) - -// Valid indicates whether the value is a known member of the V1GetServicesHealthParamsServices enum. -func (e V1GetServicesHealthParamsServices) Valid() bool { - switch e { - case V1GetServicesHealthParamsServicesAuth: - return true - case V1GetServicesHealthParamsServicesDb: - return true - case V1GetServicesHealthParamsServicesDbPostgresUser: - return true - case V1GetServicesHealthParamsServicesPgBouncer: - return true - case V1GetServicesHealthParamsServicesPooler: - return true - case V1GetServicesHealthParamsServicesRealtime: - return true - case V1GetServicesHealthParamsServicesRest: - return true - case V1GetServicesHealthParamsServicesStorage: - return true - default: - return false - } -} - -// Defines values for V1GetJitAccessConfig200JSONResponseBody0State. -const ( - V1GetJitAccessConfig200JSONResponseBody0StateDisabled V1GetJitAccessConfig200JSONResponseBody0State = "disabled" - V1GetJitAccessConfig200JSONResponseBody0StateEnabled V1GetJitAccessConfig200JSONResponseBody0State = "enabled" -) - -// Valid indicates whether the value is a known member of the V1GetJitAccessConfig200JSONResponseBody0State enum. -func (e V1GetJitAccessConfig200JSONResponseBody0State) Valid() bool { - switch e { - case V1GetJitAccessConfig200JSONResponseBody0StateDisabled: - return true - case V1GetJitAccessConfig200JSONResponseBody0StateEnabled: - return true - default: - return false - } -} - -// Defines values for V1GetJitAccessConfig200JSONResponseBody1UnavailableReason. -const ( - V1GetJitAccessConfig200JSONResponseBody1UnavailableReasonPlatformUnsupported V1GetJitAccessConfig200JSONResponseBody1UnavailableReason = "platform_unsupported" - V1GetJitAccessConfig200JSONResponseBody1UnavailableReasonPostgresUpgradeRequired V1GetJitAccessConfig200JSONResponseBody1UnavailableReason = "postgres_upgrade_required" - V1GetJitAccessConfig200JSONResponseBody1UnavailableReasonSslEnforcementRequired V1GetJitAccessConfig200JSONResponseBody1UnavailableReason = "ssl_enforcement_required" - V1GetJitAccessConfig200JSONResponseBody1UnavailableReasonTemporarilyUnavailable V1GetJitAccessConfig200JSONResponseBody1UnavailableReason = "temporarily_unavailable" -) - -// Valid indicates whether the value is a known member of the V1GetJitAccessConfig200JSONResponseBody1UnavailableReason enum. -func (e V1GetJitAccessConfig200JSONResponseBody1UnavailableReason) Valid() bool { - switch e { - case V1GetJitAccessConfig200JSONResponseBody1UnavailableReasonPlatformUnsupported: - return true - case V1GetJitAccessConfig200JSONResponseBody1UnavailableReasonPostgresUpgradeRequired: - return true - case V1GetJitAccessConfig200JSONResponseBody1UnavailableReasonSslEnforcementRequired: - return true - case V1GetJitAccessConfig200JSONResponseBody1UnavailableReasonTemporarilyUnavailable: - return true - default: - return false - } -} - -// Defines values for V1UpdateJitAccessConfig200JSONResponseBody0State. -const ( - Disabled V1UpdateJitAccessConfig200JSONResponseBody0State = "disabled" - Enabled V1UpdateJitAccessConfig200JSONResponseBody0State = "enabled" -) - -// Valid indicates whether the value is a known member of the V1UpdateJitAccessConfig200JSONResponseBody0State enum. -func (e V1UpdateJitAccessConfig200JSONResponseBody0State) Valid() bool { - switch e { - case Disabled: - return true - case Enabled: - return true - default: - return false - } -} - -// Defines values for V1UpdateJitAccessConfig200JSONResponseBody1UnavailableReason. -const ( - V1UpdateJitAccessConfig200JSONResponseBody1UnavailableReasonPlatformUnsupported V1UpdateJitAccessConfig200JSONResponseBody1UnavailableReason = "platform_unsupported" - V1UpdateJitAccessConfig200JSONResponseBody1UnavailableReasonPostgresUpgradeRequired V1UpdateJitAccessConfig200JSONResponseBody1UnavailableReason = "postgres_upgrade_required" - V1UpdateJitAccessConfig200JSONResponseBody1UnavailableReasonSslEnforcementRequired V1UpdateJitAccessConfig200JSONResponseBody1UnavailableReason = "ssl_enforcement_required" - V1UpdateJitAccessConfig200JSONResponseBody1UnavailableReasonTemporarilyUnavailable V1UpdateJitAccessConfig200JSONResponseBody1UnavailableReason = "temporarily_unavailable" -) - -// Valid indicates whether the value is a known member of the V1UpdateJitAccessConfig200JSONResponseBody1UnavailableReason enum. -func (e V1UpdateJitAccessConfig200JSONResponseBody1UnavailableReason) Valid() bool { - switch e { - case V1UpdateJitAccessConfig200JSONResponseBody1UnavailableReasonPlatformUnsupported: - return true - case V1UpdateJitAccessConfig200JSONResponseBody1UnavailableReasonPostgresUpgradeRequired: - return true - case V1UpdateJitAccessConfig200JSONResponseBody1UnavailableReasonSslEnforcementRequired: - return true - case V1UpdateJitAccessConfig200JSONResponseBody1UnavailableReasonTemporarilyUnavailable: - return true - default: - return false - } -} - -// Defines values for V1ListAllSnippetsParamsSortBy. -const ( - InsertedAt V1ListAllSnippetsParamsSortBy = "inserted_at" - Name V1ListAllSnippetsParamsSortBy = "name" -) - -// Valid indicates whether the value is a known member of the V1ListAllSnippetsParamsSortBy enum. -func (e V1ListAllSnippetsParamsSortBy) Valid() bool { - switch e { - case InsertedAt: - return true - case Name: - return true - default: - return false - } -} - -// Defines values for V1ListAllSnippetsParamsSortOrder. -const ( - Asc V1ListAllSnippetsParamsSortOrder = "asc" - Desc V1ListAllSnippetsParamsSortOrder = "desc" -) - -// Valid indicates whether the value is a known member of the V1ListAllSnippetsParamsSortOrder enum. -func (e V1ListAllSnippetsParamsSortOrder) Valid() bool { - switch e { - case Asc: - return true - case Desc: - return true - default: - return false - } -} - -// AcceptInviteExternalUserJitAccessBody defines model for AcceptInviteExternalUserJitAccessBody. -type AcceptInviteExternalUserJitAccessBody struct { - Email openapi_types.Email `json:"email"` - Token string `json:"token"` -} - -// ActionRunResponseOutput defines model for ActionRunResponse_Output. -type ActionRunResponseOutput struct { - BranchId string `json:"branch_id"` - CheckRunId nullable.Nullable[float32] `json:"check_run_id"` - CreatedAt string `json:"created_at"` - GitConfig nullable.Nullable[interface{}] `json:"git_config,omitempty"` - Id string `json:"id"` - RunSteps []struct { - CreatedAt string `json:"created_at"` - Name ActionRunResponseOutputRunStepsName `json:"name"` - Status ActionRunResponseOutputRunStepsStatus `json:"status"` - UpdatedAt string `json:"updated_at"` - } `json:"run_steps"` - UpdatedAt string `json:"updated_at"` - Workdir nullable.Nullable[string] `json:"workdir"` -} - -// ActionRunResponseOutputRunStepsName defines model for ActionRunResponseOutput.RunSteps.Name. -type ActionRunResponseOutputRunStepsName string - -// ActionRunResponseOutputRunStepsStatus defines model for ActionRunResponseOutput.RunSteps.Status. -type ActionRunResponseOutputRunStepsStatus string - -// ActivateVanitySubdomainResponseOutput defines model for ActivateVanitySubdomainResponse_Output. -type ActivateVanitySubdomainResponseOutput struct { - CustomDomain string `json:"custom_domain"` -} - -// AnalyticsResponseOutput defines model for AnalyticsResponse_Output. -type AnalyticsResponseOutput struct { - Error *AnalyticsResponseOutput_Error `json:"error,omitempty"` - Result *[]interface{} `json:"result,omitempty"` -} - -// AnalyticsResponseOutputError0 defines model for . -type AnalyticsResponseOutputError0 = string - -// AnalyticsResponseOutputError1 defines model for . -type AnalyticsResponseOutputError1 struct { - Code float32 `json:"code"` - Errors []struct { - Domain string `json:"domain"` - Location string `json:"location"` - LocationType string `json:"locationType"` - Message string `json:"message"` - Reason string `json:"reason"` - } `json:"errors"` - Message string `json:"message"` - Status string `json:"status"` -} - -// AnalyticsResponseOutput_Error defines model for AnalyticsResponseOutput.Error. -type AnalyticsResponseOutput_Error struct { - union json.RawMessage -} - -// ApiKeyResponseOutput defines model for ApiKeyResponse_Output. -type ApiKeyResponseOutput struct { - ApiKey nullable.Nullable[string] `json:"api_key,omitempty"` - Description nullable.Nullable[string] `json:"description,omitempty"` - Hash nullable.Nullable[string] `json:"hash,omitempty"` - Id nullable.Nullable[string] `json:"id,omitempty"` - InsertedAt nullable.Nullable[time.Time] `json:"inserted_at,omitempty"` - Name string `json:"name"` - Prefix nullable.Nullable[string] `json:"prefix,omitempty"` - SecretJwtTemplate nullable.Nullable[map[string]interface{}] `json:"secret_jwt_template,omitempty"` - Type nullable.Nullable[ApiKeyResponseOutputType] `json:"type,omitempty"` - UpdatedAt nullable.Nullable[time.Time] `json:"updated_at,omitempty"` -} - -// ApiKeyResponseOutputType defines model for ApiKeyResponseOutput.Type. -type ApiKeyResponseOutputType string - -// ApplyProjectAddonBody defines model for ApplyProjectAddonBody. -type ApplyProjectAddonBody struct { - AddonType ApplyProjectAddonBodyAddonType `json:"addon_type"` - AddonVariant ApplyProjectAddonBody_AddonVariant `json:"addon_variant"` -} - -// ApplyProjectAddonBodyAddonType defines model for ApplyProjectAddonBody.AddonType. -type ApplyProjectAddonBodyAddonType string - -// ApplyProjectAddonBodyAddonVariant0 defines model for ApplyProjectAddonBody.AddonVariant.0. -type ApplyProjectAddonBodyAddonVariant0 string - -// ApplyProjectAddonBodyAddonVariant1 defines model for ApplyProjectAddonBody.AddonVariant.1. -type ApplyProjectAddonBodyAddonVariant1 string - -// ApplyProjectAddonBodyAddonVariant2 defines model for ApplyProjectAddonBody.AddonVariant.2. -type ApplyProjectAddonBodyAddonVariant2 string - -// ApplyProjectAddonBodyAddonVariant3 defines model for ApplyProjectAddonBody.AddonVariant.3. -type ApplyProjectAddonBodyAddonVariant3 string - -// ApplyProjectAddonBody_AddonVariant defines model for ApplyProjectAddonBody.AddonVariant. -type ApplyProjectAddonBody_AddonVariant struct { - union json.RawMessage -} - -// AuthConfigResponseOutput defines model for AuthConfigResponse_Output. -type AuthConfigResponseOutput struct { - ApiMaxRequestDuration nullable.Nullable[int] `json:"api_max_request_duration"` - CustomOauthEnabled bool `json:"custom_oauth_enabled"` - CustomOauthMaxProviders int `json:"custom_oauth_max_providers"` - DbMaxPoolSize nullable.Nullable[int] `json:"db_max_pool_size"` - DbMaxPoolSizeUnit nullable.Nullable[AuthConfigResponseOutputDbMaxPoolSizeUnit] `json:"db_max_pool_size_unit"` - DisableSignup nullable.Nullable[bool] `json:"disable_signup"` - ExternalAnonymousUsersEnabled nullable.Nullable[bool] `json:"external_anonymous_users_enabled"` - ExternalAppleAdditionalClientIds nullable.Nullable[string] `json:"external_apple_additional_client_ids"` - ExternalAppleClientId nullable.Nullable[string] `json:"external_apple_client_id"` - ExternalAppleEmailOptional nullable.Nullable[bool] `json:"external_apple_email_optional"` - ExternalAppleEnabled nullable.Nullable[bool] `json:"external_apple_enabled"` - ExternalAppleSecret nullable.Nullable[string] `json:"external_apple_secret"` - ExternalAzureClientId nullable.Nullable[string] `json:"external_azure_client_id"` - ExternalAzureEmailOptional nullable.Nullable[bool] `json:"external_azure_email_optional"` - ExternalAzureEnabled nullable.Nullable[bool] `json:"external_azure_enabled"` - ExternalAzureSecret nullable.Nullable[string] `json:"external_azure_secret"` - ExternalAzureUrl nullable.Nullable[string] `json:"external_azure_url"` - ExternalBitbucketClientId nullable.Nullable[string] `json:"external_bitbucket_client_id"` - ExternalBitbucketEmailOptional nullable.Nullable[bool] `json:"external_bitbucket_email_optional"` - ExternalBitbucketEnabled nullable.Nullable[bool] `json:"external_bitbucket_enabled"` - ExternalBitbucketSecret nullable.Nullable[string] `json:"external_bitbucket_secret"` - ExternalDiscordClientId nullable.Nullable[string] `json:"external_discord_client_id"` - ExternalDiscordEmailOptional nullable.Nullable[bool] `json:"external_discord_email_optional"` - ExternalDiscordEnabled nullable.Nullable[bool] `json:"external_discord_enabled"` - ExternalDiscordSecret nullable.Nullable[string] `json:"external_discord_secret"` - ExternalEmailEnabled nullable.Nullable[bool] `json:"external_email_enabled"` - ExternalFacebookClientId nullable.Nullable[string] `json:"external_facebook_client_id"` - ExternalFacebookEmailOptional nullable.Nullable[bool] `json:"external_facebook_email_optional"` - ExternalFacebookEnabled nullable.Nullable[bool] `json:"external_facebook_enabled"` - ExternalFacebookSecret nullable.Nullable[string] `json:"external_facebook_secret"` - ExternalFigmaClientId nullable.Nullable[string] `json:"external_figma_client_id"` - ExternalFigmaEmailOptional nullable.Nullable[bool] `json:"external_figma_email_optional"` - ExternalFigmaEnabled nullable.Nullable[bool] `json:"external_figma_enabled"` - ExternalFigmaSecret nullable.Nullable[string] `json:"external_figma_secret"` - ExternalGithubClientId nullable.Nullable[string] `json:"external_github_client_id"` - ExternalGithubEmailOptional nullable.Nullable[bool] `json:"external_github_email_optional"` - ExternalGithubEnabled nullable.Nullable[bool] `json:"external_github_enabled"` - ExternalGithubSecret nullable.Nullable[string] `json:"external_github_secret"` - ExternalGitlabClientId nullable.Nullable[string] `json:"external_gitlab_client_id"` - ExternalGitlabEmailOptional nullable.Nullable[bool] `json:"external_gitlab_email_optional"` - ExternalGitlabEnabled nullable.Nullable[bool] `json:"external_gitlab_enabled"` - ExternalGitlabSecret nullable.Nullable[string] `json:"external_gitlab_secret"` - ExternalGitlabUrl nullable.Nullable[string] `json:"external_gitlab_url"` - ExternalGoogleAdditionalClientIds nullable.Nullable[string] `json:"external_google_additional_client_ids"` - ExternalGoogleClientId nullable.Nullable[string] `json:"external_google_client_id"` - ExternalGoogleEmailOptional nullable.Nullable[bool] `json:"external_google_email_optional"` - ExternalGoogleEnabled nullable.Nullable[bool] `json:"external_google_enabled"` - ExternalGoogleSecret nullable.Nullable[string] `json:"external_google_secret"` - ExternalGoogleSkipNonceCheck nullable.Nullable[bool] `json:"external_google_skip_nonce_check"` - ExternalKakaoClientId nullable.Nullable[string] `json:"external_kakao_client_id"` - ExternalKakaoEmailOptional nullable.Nullable[bool] `json:"external_kakao_email_optional"` - ExternalKakaoEnabled nullable.Nullable[bool] `json:"external_kakao_enabled"` - ExternalKakaoSecret nullable.Nullable[string] `json:"external_kakao_secret"` - ExternalKeycloakClientId nullable.Nullable[string] `json:"external_keycloak_client_id"` - ExternalKeycloakEmailOptional nullable.Nullable[bool] `json:"external_keycloak_email_optional"` - ExternalKeycloakEnabled nullable.Nullable[bool] `json:"external_keycloak_enabled"` - ExternalKeycloakSecret nullable.Nullable[string] `json:"external_keycloak_secret"` - ExternalKeycloakUrl nullable.Nullable[string] `json:"external_keycloak_url"` - ExternalLinkedinOidcClientId nullable.Nullable[string] `json:"external_linkedin_oidc_client_id"` - ExternalLinkedinOidcEmailOptional nullable.Nullable[bool] `json:"external_linkedin_oidc_email_optional"` - ExternalLinkedinOidcEnabled nullable.Nullable[bool] `json:"external_linkedin_oidc_enabled"` - ExternalLinkedinOidcSecret nullable.Nullable[string] `json:"external_linkedin_oidc_secret"` - ExternalNotionClientId nullable.Nullable[string] `json:"external_notion_client_id"` - ExternalNotionEmailOptional nullable.Nullable[bool] `json:"external_notion_email_optional"` - ExternalNotionEnabled nullable.Nullable[bool] `json:"external_notion_enabled"` - ExternalNotionSecret nullable.Nullable[string] `json:"external_notion_secret"` - ExternalPhoneEnabled nullable.Nullable[bool] `json:"external_phone_enabled"` - ExternalSlackClientId nullable.Nullable[string] `json:"external_slack_client_id"` - ExternalSlackEmailOptional nullable.Nullable[bool] `json:"external_slack_email_optional"` - ExternalSlackEnabled nullable.Nullable[bool] `json:"external_slack_enabled"` - ExternalSlackOidcClientId nullable.Nullable[string] `json:"external_slack_oidc_client_id"` - ExternalSlackOidcEmailOptional nullable.Nullable[bool] `json:"external_slack_oidc_email_optional"` - ExternalSlackOidcEnabled nullable.Nullable[bool] `json:"external_slack_oidc_enabled"` - ExternalSlackOidcSecret nullable.Nullable[string] `json:"external_slack_oidc_secret"` - ExternalSlackSecret nullable.Nullable[string] `json:"external_slack_secret"` - ExternalSpotifyClientId nullable.Nullable[string] `json:"external_spotify_client_id"` - ExternalSpotifyEmailOptional nullable.Nullable[bool] `json:"external_spotify_email_optional"` - ExternalSpotifyEnabled nullable.Nullable[bool] `json:"external_spotify_enabled"` - ExternalSpotifySecret nullable.Nullable[string] `json:"external_spotify_secret"` - ExternalTwitchClientId nullable.Nullable[string] `json:"external_twitch_client_id"` - ExternalTwitchEmailOptional nullable.Nullable[bool] `json:"external_twitch_email_optional"` - ExternalTwitchEnabled nullable.Nullable[bool] `json:"external_twitch_enabled"` - ExternalTwitchSecret nullable.Nullable[string] `json:"external_twitch_secret"` - ExternalTwitterClientId nullable.Nullable[string] `json:"external_twitter_client_id"` - ExternalTwitterEmailOptional nullable.Nullable[bool] `json:"external_twitter_email_optional"` - ExternalTwitterEnabled nullable.Nullable[bool] `json:"external_twitter_enabled"` - ExternalTwitterSecret nullable.Nullable[string] `json:"external_twitter_secret"` - ExternalWeb3EthereumEnabled nullable.Nullable[bool] `json:"external_web3_ethereum_enabled"` - ExternalWeb3SolanaEnabled nullable.Nullable[bool] `json:"external_web3_solana_enabled"` - ExternalWorkosClientId nullable.Nullable[string] `json:"external_workos_client_id"` - ExternalWorkosEnabled nullable.Nullable[bool] `json:"external_workos_enabled"` - ExternalWorkosSecret nullable.Nullable[string] `json:"external_workos_secret"` - ExternalWorkosUrl nullable.Nullable[string] `json:"external_workos_url"` - ExternalXClientId nullable.Nullable[string] `json:"external_x_client_id"` - ExternalXEmailOptional nullable.Nullable[bool] `json:"external_x_email_optional"` - ExternalXEnabled nullable.Nullable[bool] `json:"external_x_enabled"` - ExternalXSecret nullable.Nullable[string] `json:"external_x_secret"` - ExternalZoomClientId nullable.Nullable[string] `json:"external_zoom_client_id"` - ExternalZoomEmailOptional nullable.Nullable[bool] `json:"external_zoom_email_optional"` - ExternalZoomEnabled nullable.Nullable[bool] `json:"external_zoom_enabled"` - ExternalZoomSecret nullable.Nullable[string] `json:"external_zoom_secret"` - HookAfterUserCreatedEnabled nullable.Nullable[bool] `json:"hook_after_user_created_enabled"` - HookAfterUserCreatedSecrets nullable.Nullable[string] `json:"hook_after_user_created_secrets"` - HookAfterUserCreatedUri nullable.Nullable[string] `json:"hook_after_user_created_uri"` - HookBeforeUserCreatedEnabled nullable.Nullable[bool] `json:"hook_before_user_created_enabled"` - HookBeforeUserCreatedSecrets nullable.Nullable[string] `json:"hook_before_user_created_secrets"` - HookBeforeUserCreatedUri nullable.Nullable[string] `json:"hook_before_user_created_uri"` - HookCustomAccessTokenEnabled nullable.Nullable[bool] `json:"hook_custom_access_token_enabled"` - HookCustomAccessTokenSecrets nullable.Nullable[string] `json:"hook_custom_access_token_secrets"` - HookCustomAccessTokenUri nullable.Nullable[string] `json:"hook_custom_access_token_uri"` - HookMfaVerificationAttemptEnabled nullable.Nullable[bool] `json:"hook_mfa_verification_attempt_enabled"` - HookMfaVerificationAttemptSecrets nullable.Nullable[string] `json:"hook_mfa_verification_attempt_secrets"` - HookMfaVerificationAttemptUri nullable.Nullable[string] `json:"hook_mfa_verification_attempt_uri"` - HookPasswordVerificationAttemptEnabled nullable.Nullable[bool] `json:"hook_password_verification_attempt_enabled"` - HookPasswordVerificationAttemptSecrets nullable.Nullable[string] `json:"hook_password_verification_attempt_secrets"` - HookPasswordVerificationAttemptUri nullable.Nullable[string] `json:"hook_password_verification_attempt_uri"` - HookSendEmailEnabled nullable.Nullable[bool] `json:"hook_send_email_enabled"` - HookSendEmailSecrets nullable.Nullable[string] `json:"hook_send_email_secrets"` - HookSendEmailUri nullable.Nullable[string] `json:"hook_send_email_uri"` - HookSendSmsEnabled nullable.Nullable[bool] `json:"hook_send_sms_enabled"` - HookSendSmsSecrets nullable.Nullable[string] `json:"hook_send_sms_secrets"` - HookSendSmsUri nullable.Nullable[string] `json:"hook_send_sms_uri"` - JwtExp nullable.Nullable[int] `json:"jwt_exp"` - MailerAllowUnverifiedEmailSignIns nullable.Nullable[bool] `json:"mailer_allow_unverified_email_sign_ins"` - MailerAutoconfirm nullable.Nullable[bool] `json:"mailer_autoconfirm"` - MailerNotificationsEmailChangedEnabled nullable.Nullable[bool] `json:"mailer_notifications_email_changed_enabled"` - MailerNotificationsIdentityLinkedEnabled nullable.Nullable[bool] `json:"mailer_notifications_identity_linked_enabled"` - MailerNotificationsIdentityUnlinkedEnabled nullable.Nullable[bool] `json:"mailer_notifications_identity_unlinked_enabled"` - MailerNotificationsMfaFactorEnrolledEnabled nullable.Nullable[bool] `json:"mailer_notifications_mfa_factor_enrolled_enabled"` - MailerNotificationsMfaFactorUnenrolledEnabled nullable.Nullable[bool] `json:"mailer_notifications_mfa_factor_unenrolled_enabled"` - MailerNotificationsPasswordChangedEnabled nullable.Nullable[bool] `json:"mailer_notifications_password_changed_enabled"` - MailerNotificationsPhoneChangedEnabled nullable.Nullable[bool] `json:"mailer_notifications_phone_changed_enabled"` - MailerOtpExp int `json:"mailer_otp_exp"` - MailerOtpLength nullable.Nullable[int] `json:"mailer_otp_length"` - MailerSecureEmailChangeEnabled nullable.Nullable[bool] `json:"mailer_secure_email_change_enabled"` - MailerSubjectsConfirmation nullable.Nullable[string] `json:"mailer_subjects_confirmation"` - MailerSubjectsEmailChange nullable.Nullable[string] `json:"mailer_subjects_email_change"` - MailerSubjectsEmailChangedNotification nullable.Nullable[string] `json:"mailer_subjects_email_changed_notification"` - MailerSubjectsIdentityLinkedNotification nullable.Nullable[string] `json:"mailer_subjects_identity_linked_notification"` - MailerSubjectsIdentityUnlinkedNotification nullable.Nullable[string] `json:"mailer_subjects_identity_unlinked_notification"` - MailerSubjectsInvite nullable.Nullable[string] `json:"mailer_subjects_invite"` - MailerSubjectsMagicLink nullable.Nullable[string] `json:"mailer_subjects_magic_link"` - MailerSubjectsMfaFactorEnrolledNotification nullable.Nullable[string] `json:"mailer_subjects_mfa_factor_enrolled_notification"` - MailerSubjectsMfaFactorUnenrolledNotification nullable.Nullable[string] `json:"mailer_subjects_mfa_factor_unenrolled_notification"` - MailerSubjectsPasswordChangedNotification nullable.Nullable[string] `json:"mailer_subjects_password_changed_notification"` - MailerSubjectsPhoneChangedNotification nullable.Nullable[string] `json:"mailer_subjects_phone_changed_notification"` - MailerSubjectsReauthentication nullable.Nullable[string] `json:"mailer_subjects_reauthentication"` - MailerSubjectsRecovery nullable.Nullable[string] `json:"mailer_subjects_recovery"` - MailerTemplatesConfirmationContent nullable.Nullable[string] `json:"mailer_templates_confirmation_content"` - MailerTemplatesEmailChangeContent nullable.Nullable[string] `json:"mailer_templates_email_change_content"` - MailerTemplatesEmailChangedNotificationContent nullable.Nullable[string] `json:"mailer_templates_email_changed_notification_content"` - MailerTemplatesIdentityLinkedNotificationContent nullable.Nullable[string] `json:"mailer_templates_identity_linked_notification_content"` - MailerTemplatesIdentityUnlinkedNotificationContent nullable.Nullable[string] `json:"mailer_templates_identity_unlinked_notification_content"` - MailerTemplatesInviteContent nullable.Nullable[string] `json:"mailer_templates_invite_content"` - MailerTemplatesMagicLinkContent nullable.Nullable[string] `json:"mailer_templates_magic_link_content"` - MailerTemplatesMfaFactorEnrolledNotificationContent nullable.Nullable[string] `json:"mailer_templates_mfa_factor_enrolled_notification_content"` - MailerTemplatesMfaFactorUnenrolledNotificationContent nullable.Nullable[string] `json:"mailer_templates_mfa_factor_unenrolled_notification_content"` - MailerTemplatesPasswordChangedNotificationContent nullable.Nullable[string] `json:"mailer_templates_password_changed_notification_content"` - MailerTemplatesPhoneChangedNotificationContent nullable.Nullable[string] `json:"mailer_templates_phone_changed_notification_content"` - MailerTemplatesReauthenticationContent nullable.Nullable[string] `json:"mailer_templates_reauthentication_content"` - MailerTemplatesRecoveryContent nullable.Nullable[string] `json:"mailer_templates_recovery_content"` - MfaMaxEnrolledFactors nullable.Nullable[int] `json:"mfa_max_enrolled_factors"` - MfaPhoneEnrollEnabled nullable.Nullable[bool] `json:"mfa_phone_enroll_enabled"` - MfaPhoneMaxFrequency nullable.Nullable[int] `json:"mfa_phone_max_frequency"` - MfaPhoneOtpLength int `json:"mfa_phone_otp_length"` - MfaPhoneTemplate nullable.Nullable[string] `json:"mfa_phone_template"` - MfaPhoneVerifyEnabled nullable.Nullable[bool] `json:"mfa_phone_verify_enabled"` - MfaTotpEnrollEnabled nullable.Nullable[bool] `json:"mfa_totp_enroll_enabled"` - MfaTotpVerifyEnabled nullable.Nullable[bool] `json:"mfa_totp_verify_enabled"` - MfaWebAuthnEnrollEnabled nullable.Nullable[bool] `json:"mfa_web_authn_enroll_enabled"` - MfaWebAuthnVerifyEnabled nullable.Nullable[bool] `json:"mfa_web_authn_verify_enabled"` - NimbusOauthClientId nullable.Nullable[string] `json:"nimbus_oauth_client_id"` - NimbusOauthClientSecret nullable.Nullable[string] `json:"nimbus_oauth_client_secret"` - NimbusOauthEmailOptional nullable.Nullable[bool] `json:"nimbus_oauth_email_optional"` - OauthServerAllowDynamicRegistration bool `json:"oauth_server_allow_dynamic_registration"` - OauthServerAuthorizationPath nullable.Nullable[string] `json:"oauth_server_authorization_path"` - OauthServerEnabled bool `json:"oauth_server_enabled"` - PasskeyEnabled bool `json:"passkey_enabled"` - PasswordHibpEnabled nullable.Nullable[bool] `json:"password_hibp_enabled"` - PasswordMinLength nullable.Nullable[int] `json:"password_min_length"` - PasswordRequiredCharacters nullable.Nullable[AuthConfigResponseOutputPasswordRequiredCharacters] `json:"password_required_characters"` - RateLimitAnonymousUsers nullable.Nullable[int] `json:"rate_limit_anonymous_users"` - RateLimitEmailSent nullable.Nullable[int] `json:"rate_limit_email_sent"` - RateLimitOtp nullable.Nullable[int] `json:"rate_limit_otp"` - RateLimitSmsSent nullable.Nullable[int] `json:"rate_limit_sms_sent"` - RateLimitTokenRefresh nullable.Nullable[int] `json:"rate_limit_token_refresh"` - RateLimitVerify nullable.Nullable[int] `json:"rate_limit_verify"` - RateLimitWeb3 nullable.Nullable[int] `json:"rate_limit_web3"` - RefreshTokenRotationEnabled nullable.Nullable[bool] `json:"refresh_token_rotation_enabled"` - SamlAllowEncryptedAssertions nullable.Nullable[bool] `json:"saml_allow_encrypted_assertions"` - SamlEnabled nullable.Nullable[bool] `json:"saml_enabled"` - SamlExternalUrl nullable.Nullable[string] `json:"saml_external_url"` - SecurityCaptchaEnabled nullable.Nullable[bool] `json:"security_captcha_enabled"` - SecurityCaptchaProvider nullable.Nullable[AuthConfigResponseOutputSecurityCaptchaProvider] `json:"security_captcha_provider"` - SecurityCaptchaSecret nullable.Nullable[string] `json:"security_captcha_secret"` - SecurityManualLinkingEnabled nullable.Nullable[bool] `json:"security_manual_linking_enabled"` - SecurityRefreshTokenReuseInterval nullable.Nullable[int] `json:"security_refresh_token_reuse_interval"` - SecuritySbForwardedForEnabled nullable.Nullable[bool] `json:"security_sb_forwarded_for_enabled"` - SecurityUpdatePasswordRequireCurrentPassword nullable.Nullable[bool] `json:"security_update_password_require_current_password"` - SecurityUpdatePasswordRequireReauthentication nullable.Nullable[bool] `json:"security_update_password_require_reauthentication"` - SessionsInactivityTimeout nullable.Nullable[float32] `json:"sessions_inactivity_timeout"` - SessionsSinglePerUser nullable.Nullable[bool] `json:"sessions_single_per_user"` - SessionsTags nullable.Nullable[string] `json:"sessions_tags"` - SessionsTimebox nullable.Nullable[float32] `json:"sessions_timebox"` - SiteUrl nullable.Nullable[string] `json:"site_url"` - SmsAutoconfirm nullable.Nullable[bool] `json:"sms_autoconfirm"` - SmsMaxFrequency nullable.Nullable[int] `json:"sms_max_frequency"` - SmsMessagebirdAccessKey nullable.Nullable[string] `json:"sms_messagebird_access_key"` - SmsMessagebirdOriginator nullable.Nullable[string] `json:"sms_messagebird_originator"` - SmsOtpExp nullable.Nullable[int] `json:"sms_otp_exp"` - SmsOtpLength int `json:"sms_otp_length"` - SmsProvider nullable.Nullable[AuthConfigResponseOutputSmsProvider] `json:"sms_provider"` - SmsTemplate nullable.Nullable[string] `json:"sms_template"` - SmsTestOtp nullable.Nullable[string] `json:"sms_test_otp"` - SmsTestOtpValidUntil nullable.Nullable[time.Time] `json:"sms_test_otp_valid_until"` - SmsTextlocalApiKey nullable.Nullable[string] `json:"sms_textlocal_api_key"` - SmsTextlocalSender nullable.Nullable[string] `json:"sms_textlocal_sender"` - SmsTwilioAccountSid nullable.Nullable[string] `json:"sms_twilio_account_sid"` - SmsTwilioAuthToken nullable.Nullable[string] `json:"sms_twilio_auth_token"` - SmsTwilioContentSid nullable.Nullable[string] `json:"sms_twilio_content_sid"` - SmsTwilioMessageServiceSid nullable.Nullable[string] `json:"sms_twilio_message_service_sid"` - SmsTwilioVerifyAccountSid nullable.Nullable[string] `json:"sms_twilio_verify_account_sid"` - SmsTwilioVerifyAuthToken nullable.Nullable[string] `json:"sms_twilio_verify_auth_token"` - SmsTwilioVerifyMessageServiceSid nullable.Nullable[string] `json:"sms_twilio_verify_message_service_sid"` - SmsVonageApiKey nullable.Nullable[string] `json:"sms_vonage_api_key"` - SmsVonageApiSecret nullable.Nullable[string] `json:"sms_vonage_api_secret"` - SmsVonageFrom nullable.Nullable[string] `json:"sms_vonage_from"` - SmtpAdminEmail nullable.Nullable[openapi_types.Email] `json:"smtp_admin_email"` - SmtpHost nullable.Nullable[string] `json:"smtp_host"` - SmtpMaxFrequency nullable.Nullable[int] `json:"smtp_max_frequency"` - SmtpPass nullable.Nullable[string] `json:"smtp_pass"` - SmtpPort nullable.Nullable[string] `json:"smtp_port"` - SmtpSenderName nullable.Nullable[string] `json:"smtp_sender_name"` - SmtpUser nullable.Nullable[string] `json:"smtp_user"` - UriAllowList nullable.Nullable[string] `json:"uri_allow_list"` - WebauthnRpDisplayName nullable.Nullable[string] `json:"webauthn_rp_display_name"` - WebauthnRpId nullable.Nullable[string] `json:"webauthn_rp_id"` - WebauthnRpOrigins nullable.Nullable[string] `json:"webauthn_rp_origins"` -} - -// AuthConfigResponseOutputDbMaxPoolSizeUnit defines model for AuthConfigResponseOutput.DbMaxPoolSizeUnit. -type AuthConfigResponseOutputDbMaxPoolSizeUnit string - -// AuthConfigResponseOutputPasswordRequiredCharacters defines model for AuthConfigResponseOutput.PasswordRequiredCharacters. -type AuthConfigResponseOutputPasswordRequiredCharacters string - -// AuthConfigResponseOutputSecurityCaptchaProvider defines model for AuthConfigResponseOutput.SecurityCaptchaProvider. -type AuthConfigResponseOutputSecurityCaptchaProvider string - -// AuthConfigResponseOutputSmsProvider defines model for AuthConfigResponseOutput.SmsProvider. -type AuthConfigResponseOutputSmsProvider string - -// AuthorizeJitAccessBody defines model for AuthorizeJitAccessBody. -type AuthorizeJitAccessBody struct { - Rhost AuthorizeJitAccessBody_Rhost `json:"rhost"` - Role string `json:"role"` -} - -// AuthorizeJitAccessBodyRhost0 defines model for . -type AuthorizeJitAccessBodyRhost0 = string - -// AuthorizeJitAccessBodyRhost1 defines model for . -type AuthorizeJitAccessBodyRhost1 = string - -// AuthorizeJitAccessBody_Rhost defines model for AuthorizeJitAccessBody.Rhost. -type AuthorizeJitAccessBody_Rhost struct { - union json.RawMessage -} - -// BranchActionBody defines model for BranchActionBody. -type BranchActionBody struct { - MigrationVersion *string `json:"migration_version,omitempty"` -} - -// BranchDeleteResponseOutput defines model for BranchDeleteResponse_Output. -type BranchDeleteResponseOutput struct { - Message BranchDeleteResponseOutputMessage `json:"message"` -} - -// BranchDeleteResponseOutputMessage defines model for BranchDeleteResponseOutput.Message. -type BranchDeleteResponseOutputMessage string - -// BranchDetailResponseOutput defines model for BranchDetailResponse_Output. -type BranchDetailResponseOutput struct { - DbHost string `json:"db_host"` - DbPass *string `json:"db_pass,omitempty"` - DbPort int `json:"db_port"` - DbUser *string `json:"db_user,omitempty"` - JwtSecret *string `json:"jwt_secret,omitempty"` - PostgresEngine string `json:"postgres_engine"` - PostgresVersion string `json:"postgres_version"` - Ref string `json:"ref"` - ReleaseChannel string `json:"release_channel"` - Status BranchDetailResponseOutputStatus `json:"status"` -} - -// BranchDetailResponseOutputStatus defines model for BranchDetailResponseOutput.Status. -type BranchDetailResponseOutputStatus string - -// BranchResponseOutput defines model for BranchResponse_Output. -type BranchResponseOutput struct { - CreatedAt time.Time `json:"created_at"` - DeletionScheduledAt *time.Time `json:"deletion_scheduled_at,omitempty"` - GitBranch *string `json:"git_branch,omitempty"` - Id openapi_types.UUID `json:"id"` - IsDefault bool `json:"is_default"` - - // LatestCheckRunId This field is deprecated and will not be populated. - // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set - LatestCheckRunId *float32 `json:"latest_check_run_id,omitempty"` - Name string `json:"name"` - NotifyUrl *string `json:"notify_url,omitempty"` - ParentProjectRef string `json:"parent_project_ref"` - Persistent bool `json:"persistent"` - PrNumber *int32 `json:"pr_number,omitempty"` - PreviewProjectStatus *BranchResponseOutputPreviewProjectStatus `json:"preview_project_status,omitempty"` - ProjectRef string `json:"project_ref"` - ReviewRequestedAt *time.Time `json:"review_requested_at,omitempty"` - - // Status This field is deprecated. List action runs to get branch status instead. - // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set - Status BranchResponseOutputStatus `json:"status"` - UpdatedAt time.Time `json:"updated_at"` - WithData bool `json:"with_data"` -} - -// BranchResponseOutputPreviewProjectStatus defines model for BranchResponseOutput.PreviewProjectStatus. -type BranchResponseOutputPreviewProjectStatus string - -// BranchResponseOutputStatus This field is deprecated. List action runs to get branch status instead. -type BranchResponseOutputStatus string - -// BranchRestoreResponseOutput defines model for BranchRestoreResponse_Output. -type BranchRestoreResponseOutput struct { - Message BranchRestoreResponseOutputMessage `json:"message"` -} - -// BranchRestoreResponseOutputMessage defines model for BranchRestoreResponseOutput.Message. -type BranchRestoreResponseOutputMessage string - -// BranchUpdateResponseOutput defines model for BranchUpdateResponse_Output. -type BranchUpdateResponseOutput struct { - Message BranchUpdateResponseOutputMessage `json:"message"` - WorkflowRunId string `json:"workflow_run_id"` -} - -// BranchUpdateResponseOutputMessage defines model for BranchUpdateResponseOutput.Message. -type BranchUpdateResponseOutputMessage string - -// BulkUpdateFunctionBody defines model for BulkUpdateFunctionBody. -type BulkUpdateFunctionBody = []struct { - CreatedAt *int64 `json:"created_at,omitempty"` - EntrypointPath *string `json:"entrypoint_path,omitempty"` - EzbrSha256 *string `json:"ezbr_sha256,omitempty"` - Id string `json:"id"` - ImportMap *bool `json:"import_map,omitempty"` - ImportMapPath *string `json:"import_map_path,omitempty"` - Name string `json:"name"` - Slug string `json:"slug"` - Status BulkUpdateFunctionBodyStatus `json:"status"` - VerifyJwt *bool `json:"verify_jwt,omitempty"` - Version int `json:"version"` -} - -// BulkUpdateFunctionBodyStatus defines model for BulkUpdateFunctionBody.Status. -type BulkUpdateFunctionBodyStatus string - -// BulkUpdateFunctionResponseOutput defines model for BulkUpdateFunctionResponse_Output. -type BulkUpdateFunctionResponseOutput struct { - Functions []struct { - CreatedAt int64 `json:"created_at"` - EntrypointPath *string `json:"entrypoint_path,omitempty"` - EzbrSha256 *string `json:"ezbr_sha256,omitempty"` - Id string `json:"id"` - ImportMap *bool `json:"import_map,omitempty"` - ImportMapPath *string `json:"import_map_path,omitempty"` - Name string `json:"name"` - Slug string `json:"slug"` - Status BulkUpdateFunctionResponseOutputFunctionsStatus `json:"status"` - UpdatedAt int64 `json:"updated_at"` - VerifyJwt *bool `json:"verify_jwt,omitempty"` - Version int `json:"version"` - } `json:"functions"` -} - -// BulkUpdateFunctionResponseOutputFunctionsStatus defines model for BulkUpdateFunctionResponseOutput.Functions.Status. -type BulkUpdateFunctionResponseOutputFunctionsStatus string - -// CreateApiKeyBody defines model for CreateApiKeyBody. -type CreateApiKeyBody struct { - Description nullable.Nullable[string] `json:"description,omitempty"` - Name string `json:"name"` - SecretJwtTemplate nullable.Nullable[map[string]interface{}] `json:"secret_jwt_template,omitempty"` - Type CreateApiKeyBodyType `json:"type"` -} - -// CreateApiKeyBodyType defines model for CreateApiKeyBody.Type. -type CreateApiKeyBodyType string - -// CreateBranchBody defines model for CreateBranchBody. -type CreateBranchBody struct { - BranchName string `json:"branch_name"` - DesiredInstanceSize *CreateBranchBodyDesiredInstanceSize `json:"desired_instance_size,omitempty"` - GitBranch *string `json:"git_branch,omitempty"` - IsDefault *bool `json:"is_default,omitempty"` - - // NotifyUrl HTTP endpoint to receive branch status updates. - NotifyUrl *string `json:"notify_url,omitempty"` - Persistent *bool `json:"persistent,omitempty"` - - // PostgresEngine Postgres engine version. If not provided, the latest version will be used. - PostgresEngine *CreateBranchBodyPostgresEngine `json:"postgres_engine,omitempty"` - Region *string `json:"region,omitempty"` - - // ReleaseChannel Release channel. If not provided, GA will be used. - ReleaseChannel *CreateBranchBodyReleaseChannel `json:"release_channel,omitempty"` - Secrets *map[string]string `json:"secrets,omitempty"` - WithData *bool `json:"with_data,omitempty"` -} - -// CreateBranchBodyDesiredInstanceSize defines model for CreateBranchBody.DesiredInstanceSize. -type CreateBranchBodyDesiredInstanceSize string - -// CreateBranchBodyPostgresEngine Postgres engine version. If not provided, the latest version will be used. -type CreateBranchBodyPostgresEngine string - -// CreateBranchBodyReleaseChannel Release channel. If not provided, GA will be used. -type CreateBranchBodyReleaseChannel string - -// CreateOrganizationV1 defines model for CreateOrganizationV1. -type CreateOrganizationV1 struct { - Name string `json:"name"` -} - -// CreateProjectClaimTokenResponseOutput defines model for CreateProjectClaimTokenResponse_Output. -type CreateProjectClaimTokenResponseOutput struct { - CreatedAt string `json:"created_at"` - CreatedBy openapi_types.UUID `json:"created_by"` - ExpiresAt string `json:"expires_at"` - Token string `json:"token"` - TokenAlias string `json:"token_alias"` -} - -// CreateProviderBody defines model for CreateProviderBody. -type CreateProviderBody struct { - AttributeMapping *struct { - Keys map[string]struct { - Array *bool `json:"array,omitempty"` - Default interface{} `json:"default,omitempty"` - Name *string `json:"name,omitempty"` - Names *[]string `json:"names,omitempty"` - } `json:"keys"` - } `json:"attribute_mapping,omitempty"` - Domains *[]string `json:"domains,omitempty"` - MetadataUrl *string `json:"metadata_url,omitempty"` - MetadataXml *string `json:"metadata_xml,omitempty"` - NameIdFormat *CreateProviderBodyNameIdFormat `json:"name_id_format,omitempty"` - - // Type What type of provider will be created - Type CreateProviderBodyType `json:"type"` -} - -// CreateProviderBodyNameIdFormat defines model for CreateProviderBody.NameIdFormat. -type CreateProviderBodyNameIdFormat string - -// CreateProviderBodyType What type of provider will be created -type CreateProviderBodyType string - -// CreateProviderResponseOutput defines model for CreateProviderResponse_Output. -type CreateProviderResponseOutput struct { - CreatedAt *string `json:"created_at,omitempty"` - Domains *[]struct { - CreatedAt *string `json:"created_at,omitempty"` - Domain *string `json:"domain,omitempty"` - UpdatedAt *string `json:"updated_at,omitempty"` - } `json:"domains,omitempty"` - Id string `json:"id"` - Saml *struct { - AttributeMapping *struct { - Keys map[string]struct { - Array *bool `json:"array,omitempty"` - Default interface{} `json:"default,omitempty"` - Name *string `json:"name,omitempty"` - Names *[]string `json:"names,omitempty"` - } `json:"keys"` - } `json:"attribute_mapping,omitempty"` - EntityId string `json:"entity_id"` - MetadataUrl *string `json:"metadata_url,omitempty"` - MetadataXml *string `json:"metadata_xml,omitempty"` - NameIdFormat *string `json:"name_id_format,omitempty"` - } `json:"saml,omitempty"` - UpdatedAt *string `json:"updated_at,omitempty"` -} - -// CreateRoleBody defines model for CreateRoleBody. -type CreateRoleBody struct { - ReadOnly bool `json:"read_only"` -} - -// CreateRoleResponseOutput defines model for CreateRoleResponse_Output. -type CreateRoleResponseOutput struct { - Password string `json:"password"` - Role string `json:"role"` - TtlSeconds int64 `json:"ttl_seconds"` -} - -// CreateSecretBody defines model for CreateSecretBody. -type CreateSecretBody = []struct { - // Name Secret name must not start with the SUPABASE_ prefix. - Name string `json:"name"` - Value string `json:"value"` -} - -// CreateSigningKeyBody defines model for CreateSigningKeyBody. -type CreateSigningKeyBody struct { - Algorithm CreateSigningKeyBodyAlgorithm `json:"algorithm"` - PrivateJwk *CreateSigningKeyBody_PrivateJwk `json:"private_jwk,omitempty"` - Status *CreateSigningKeyBodyStatus `json:"status,omitempty"` -} - -// CreateSigningKeyBodyAlgorithm defines model for CreateSigningKeyBody.Algorithm. -type CreateSigningKeyBodyAlgorithm string - -// CreateSigningKeyBodyPrivateJwk0 defines model for . -type CreateSigningKeyBodyPrivateJwk0 struct { - Alg *CreateSigningKeyBodyPrivateJwk0Alg `json:"alg,omitempty"` - D string `json:"d"` - Dp string `json:"dp"` - Dq string `json:"dq"` - E CreateSigningKeyBodyPrivateJwk0E `json:"e"` - Ext *CreateSigningKeyBodyPrivateJwk0Ext `json:"ext,omitempty"` - KeyOps *[]CreateSigningKeyBodyPrivateJwk0KeyOps `json:"key_ops,omitempty"` - Kid *openapi_types.UUID `json:"kid,omitempty"` - Kty CreateSigningKeyBodyPrivateJwk0Kty `json:"kty"` - N string `json:"n"` - P string `json:"p"` - Q string `json:"q"` - Qi string `json:"qi"` - Use *CreateSigningKeyBodyPrivateJwk0Use `json:"use,omitempty"` -} - -// CreateSigningKeyBodyPrivateJwk0Alg defines model for CreateSigningKeyBody.PrivateJwk.0.Alg. -type CreateSigningKeyBodyPrivateJwk0Alg string - -// CreateSigningKeyBodyPrivateJwk0E defines model for CreateSigningKeyBody.PrivateJwk.0.E. -type CreateSigningKeyBodyPrivateJwk0E string - -// CreateSigningKeyBodyPrivateJwk0Ext defines model for CreateSigningKeyBody.PrivateJwk.0.Ext. -type CreateSigningKeyBodyPrivateJwk0Ext bool - -// CreateSigningKeyBodyPrivateJwk0KeyOps defines model for CreateSigningKeyBody.PrivateJwk.0.KeyOps. -type CreateSigningKeyBodyPrivateJwk0KeyOps string - -// CreateSigningKeyBodyPrivateJwk0Kty defines model for CreateSigningKeyBody.PrivateJwk.0.Kty. -type CreateSigningKeyBodyPrivateJwk0Kty string - -// CreateSigningKeyBodyPrivateJwk0Use defines model for CreateSigningKeyBody.PrivateJwk.0.Use. -type CreateSigningKeyBodyPrivateJwk0Use string - -// CreateSigningKeyBodyPrivateJwk1 defines model for . -type CreateSigningKeyBodyPrivateJwk1 struct { - Alg *CreateSigningKeyBodyPrivateJwk1Alg `json:"alg,omitempty"` - Crv CreateSigningKeyBodyPrivateJwk1Crv `json:"crv"` - D string `json:"d"` - Ext *CreateSigningKeyBodyPrivateJwk1Ext `json:"ext,omitempty"` - KeyOps *[]CreateSigningKeyBodyPrivateJwk1KeyOps `json:"key_ops,omitempty"` - Kid *openapi_types.UUID `json:"kid,omitempty"` - Kty CreateSigningKeyBodyPrivateJwk1Kty `json:"kty"` - Use *CreateSigningKeyBodyPrivateJwk1Use `json:"use,omitempty"` - X string `json:"x"` - Y string `json:"y"` -} - -// CreateSigningKeyBodyPrivateJwk1Alg defines model for CreateSigningKeyBody.PrivateJwk.1.Alg. -type CreateSigningKeyBodyPrivateJwk1Alg string - -// CreateSigningKeyBodyPrivateJwk1Crv defines model for CreateSigningKeyBody.PrivateJwk.1.Crv. -type CreateSigningKeyBodyPrivateJwk1Crv string - -// CreateSigningKeyBodyPrivateJwk1Ext defines model for CreateSigningKeyBody.PrivateJwk.1.Ext. -type CreateSigningKeyBodyPrivateJwk1Ext bool - -// CreateSigningKeyBodyPrivateJwk1KeyOps defines model for CreateSigningKeyBody.PrivateJwk.1.KeyOps. -type CreateSigningKeyBodyPrivateJwk1KeyOps string - -// CreateSigningKeyBodyPrivateJwk1Kty defines model for CreateSigningKeyBody.PrivateJwk.1.Kty. -type CreateSigningKeyBodyPrivateJwk1Kty string - -// CreateSigningKeyBodyPrivateJwk1Use defines model for CreateSigningKeyBody.PrivateJwk.1.Use. -type CreateSigningKeyBodyPrivateJwk1Use string - -// CreateSigningKeyBodyPrivateJwk2 defines model for . -type CreateSigningKeyBodyPrivateJwk2 struct { - Alg *CreateSigningKeyBodyPrivateJwk2Alg `json:"alg,omitempty"` - Crv CreateSigningKeyBodyPrivateJwk2Crv `json:"crv"` - D string `json:"d"` - Ext *CreateSigningKeyBodyPrivateJwk2Ext `json:"ext,omitempty"` - KeyOps *[]CreateSigningKeyBodyPrivateJwk2KeyOps `json:"key_ops,omitempty"` - Kid *openapi_types.UUID `json:"kid,omitempty"` - Kty CreateSigningKeyBodyPrivateJwk2Kty `json:"kty"` - Use *CreateSigningKeyBodyPrivateJwk2Use `json:"use,omitempty"` - X string `json:"x"` -} - -// CreateSigningKeyBodyPrivateJwk2Alg defines model for CreateSigningKeyBody.PrivateJwk.2.Alg. -type CreateSigningKeyBodyPrivateJwk2Alg string - -// CreateSigningKeyBodyPrivateJwk2Crv defines model for CreateSigningKeyBody.PrivateJwk.2.Crv. -type CreateSigningKeyBodyPrivateJwk2Crv string - -// CreateSigningKeyBodyPrivateJwk2Ext defines model for CreateSigningKeyBody.PrivateJwk.2.Ext. -type CreateSigningKeyBodyPrivateJwk2Ext bool - -// CreateSigningKeyBodyPrivateJwk2KeyOps defines model for CreateSigningKeyBody.PrivateJwk.2.KeyOps. -type CreateSigningKeyBodyPrivateJwk2KeyOps string - -// CreateSigningKeyBodyPrivateJwk2Kty defines model for CreateSigningKeyBody.PrivateJwk.2.Kty. -type CreateSigningKeyBodyPrivateJwk2Kty string - -// CreateSigningKeyBodyPrivateJwk2Use defines model for CreateSigningKeyBody.PrivateJwk.2.Use. -type CreateSigningKeyBodyPrivateJwk2Use string - -// CreateSigningKeyBodyPrivateJwk3 defines model for . -type CreateSigningKeyBodyPrivateJwk3 struct { - Alg *CreateSigningKeyBodyPrivateJwk3Alg `json:"alg,omitempty"` - Ext *CreateSigningKeyBodyPrivateJwk3Ext `json:"ext,omitempty"` - K string `json:"k"` - KeyOps *[]CreateSigningKeyBodyPrivateJwk3KeyOps `json:"key_ops,omitempty"` - Kid *openapi_types.UUID `json:"kid,omitempty"` - Kty CreateSigningKeyBodyPrivateJwk3Kty `json:"kty"` - Use *CreateSigningKeyBodyPrivateJwk3Use `json:"use,omitempty"` -} - -// CreateSigningKeyBodyPrivateJwk3Alg defines model for CreateSigningKeyBody.PrivateJwk.3.Alg. -type CreateSigningKeyBodyPrivateJwk3Alg string - -// CreateSigningKeyBodyPrivateJwk3Ext defines model for CreateSigningKeyBody.PrivateJwk.3.Ext. -type CreateSigningKeyBodyPrivateJwk3Ext bool - -// CreateSigningKeyBodyPrivateJwk3KeyOps defines model for CreateSigningKeyBody.PrivateJwk.3.KeyOps. -type CreateSigningKeyBodyPrivateJwk3KeyOps string - -// CreateSigningKeyBodyPrivateJwk3Kty defines model for CreateSigningKeyBody.PrivateJwk.3.Kty. -type CreateSigningKeyBodyPrivateJwk3Kty string - -// CreateSigningKeyBodyPrivateJwk3Use defines model for CreateSigningKeyBody.PrivateJwk.3.Use. -type CreateSigningKeyBodyPrivateJwk3Use string - -// CreateSigningKeyBody_PrivateJwk defines model for CreateSigningKeyBody.PrivateJwk. -type CreateSigningKeyBody_PrivateJwk struct { - union json.RawMessage -} - -// CreateSigningKeyBodyStatus defines model for CreateSigningKeyBody.Status. -type CreateSigningKeyBodyStatus string - -// CreateThirdPartyAuthBody defines model for CreateThirdPartyAuthBody. -type CreateThirdPartyAuthBody struct { - CustomJwks interface{} `json:"custom_jwks,omitempty"` - JwksUrl *string `json:"jwks_url,omitempty"` - OidcIssuerUrl *string `json:"oidc_issuer_url,omitempty"` -} - -// DatabaseUpgradeStatusResponseOutput defines model for DatabaseUpgradeStatusResponse_Output. -type DatabaseUpgradeStatusResponseOutput struct { - DatabaseUpgradeStatus nullable.Nullable[struct { - Error *DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusError `json:"error,omitempty"` - InitiatedAt string `json:"initiated_at"` - LatestStatusAt string `json:"latest_status_at"` - Progress *DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusProgress `json:"progress,omitempty"` - Status float32 `json:"status"` - TargetVersion string `json:"target_version"` - }] `json:"databaseUpgradeStatus"` -} - -// DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusError defines model for DatabaseUpgradeStatusResponseOutput.DatabaseUpgradeStatus.Error. -type DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusError string - -// DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusProgress defines model for DatabaseUpgradeStatusResponseOutput.DatabaseUpgradeStatus.Progress. -type DatabaseUpgradeStatusResponseOutputDatabaseUpgradeStatusProgress string - -// DeleteProviderResponseOutput defines model for DeleteProviderResponse_Output. -type DeleteProviderResponseOutput struct { - CreatedAt *string `json:"created_at,omitempty"` - Domains *[]struct { - CreatedAt *string `json:"created_at,omitempty"` - Domain *string `json:"domain,omitempty"` - UpdatedAt *string `json:"updated_at,omitempty"` - } `json:"domains,omitempty"` - Id string `json:"id"` - Saml *struct { - AttributeMapping *struct { - Keys map[string]struct { - Array *bool `json:"array,omitempty"` - Default interface{} `json:"default,omitempty"` - Name *string `json:"name,omitempty"` - Names *[]string `json:"names,omitempty"` - } `json:"keys"` - } `json:"attribute_mapping,omitempty"` - EntityId string `json:"entity_id"` - MetadataUrl *string `json:"metadata_url,omitempty"` - MetadataXml *string `json:"metadata_xml,omitempty"` - NameIdFormat *string `json:"name_id_format,omitempty"` - } `json:"saml,omitempty"` - UpdatedAt *string `json:"updated_at,omitempty"` -} - -// DeleteRolesResponseOutput defines model for DeleteRolesResponse_Output. -type DeleteRolesResponseOutput struct { - Message DeleteRolesResponseOutputMessage `json:"message"` -} - -// DeleteRolesResponseOutputMessage defines model for DeleteRolesResponseOutput.Message. -type DeleteRolesResponseOutputMessage string - -// DeleteSecretsBody defines model for DeleteSecretsBody. -type DeleteSecretsBody = []string - -// DeployFunctionResponseOutput defines model for DeployFunctionResponse_Output. -type DeployFunctionResponseOutput struct { - CreatedAt *int64 `json:"created_at,omitempty"` - EntrypointPath *string `json:"entrypoint_path,omitempty"` - EzbrSha256 *string `json:"ezbr_sha256,omitempty"` - Id string `json:"id"` - ImportMap *bool `json:"import_map,omitempty"` - ImportMapPath *string `json:"import_map_path,omitempty"` - Name string `json:"name"` - Slug string `json:"slug"` - Status DeployFunctionResponseOutputStatus `json:"status"` - UpdatedAt *int64 `json:"updated_at,omitempty"` - VerifyJwt *bool `json:"verify_jwt,omitempty"` - Version int `json:"version"` -} - -// DeployFunctionResponseOutputStatus defines model for DeployFunctionResponseOutput.Status. -type DeployFunctionResponseOutputStatus string - -// DiskAutoscaleConfigOutput defines model for DiskAutoscaleConfig_Output. -type DiskAutoscaleConfigOutput struct { - // GrowthPercent Growth percentage for disk autoscaling - GrowthPercent nullable.Nullable[int] `json:"growth_percent"` - - // MaxSizeGb Maximum limit the disk size will grow to in GB - MaxSizeGb nullable.Nullable[int] `json:"max_size_gb"` - - // MinIncrementGb Minimum increment size for disk autoscaling in GB - MinIncrementGb nullable.Nullable[int] `json:"min_increment_gb"` -} - -// DiskRequestBody defines model for DiskRequestBody. -type DiskRequestBody struct { - Attributes DiskRequestBody_Attributes `json:"attributes"` -} - -// DiskRequestBodyAttributes0 defines model for . -type DiskRequestBodyAttributes0 struct { - Iops int `json:"iops"` - SizeGb int `json:"size_gb"` - ThroughputMibps *int `json:"throughput_mibps,omitempty"` - Type DiskRequestBodyAttributes0Type `json:"type"` -} - -// DiskRequestBodyAttributes0Type defines model for DiskRequestBody.Attributes.0.Type. -type DiskRequestBodyAttributes0Type string - -// DiskRequestBodyAttributes1 defines model for . -type DiskRequestBodyAttributes1 struct { - Iops int `json:"iops"` - SizeGb int `json:"size_gb"` - Type DiskRequestBodyAttributes1Type `json:"type"` -} - -// DiskRequestBodyAttributes1Type defines model for DiskRequestBody.Attributes.1.Type. -type DiskRequestBodyAttributes1Type string - -// DiskRequestBody_Attributes defines model for DiskRequestBody.Attributes. -type DiskRequestBody_Attributes struct { - union json.RawMessage -} - -// DiskResponseOutput defines model for DiskResponse_Output. -type DiskResponseOutput struct { - Attributes DiskResponseOutput_Attributes `json:"attributes"` - LastModifiedAt *string `json:"last_modified_at,omitempty"` -} - -// DiskResponseOutputAttributes0 defines model for . -type DiskResponseOutputAttributes0 struct { - Iops int `json:"iops"` - SizeGb int `json:"size_gb"` - ThroughputMibps *int `json:"throughput_mibps,omitempty"` - Type DiskResponseOutputAttributes0Type `json:"type"` -} - -// DiskResponseOutputAttributes0Type defines model for DiskResponseOutput.Attributes.0.Type. -type DiskResponseOutputAttributes0Type string - -// DiskResponseOutputAttributes1 defines model for . -type DiskResponseOutputAttributes1 struct { - Iops int `json:"iops"` - SizeGb int `json:"size_gb"` - Type DiskResponseOutputAttributes1Type `json:"type"` -} - -// DiskResponseOutputAttributes1Type defines model for DiskResponseOutput.Attributes.1.Type. -type DiskResponseOutputAttributes1Type string - -// DiskResponseOutput_Attributes defines model for DiskResponseOutput.Attributes. -type DiskResponseOutput_Attributes struct { - union json.RawMessage -} - -// DiskUtilMetricsResponseOutput defines model for DiskUtilMetricsResponse_Output. -type DiskUtilMetricsResponseOutput struct { - Metrics struct { - FsAvailBytes float32 `json:"fs_avail_bytes"` - FsSizeBytes float32 `json:"fs_size_bytes"` - FsUsedBytes float32 `json:"fs_used_bytes"` - } `json:"metrics"` - Timestamp string `json:"timestamp"` -} - -// FunctionDeployBody defines model for FunctionDeployBody. -type FunctionDeployBody struct { - File []openapi_types.File `json:"file"` - Metadata struct { - EntrypointPath string `json:"entrypoint_path"` - ImportMapPath *string `json:"import_map_path,omitempty"` - Name *string `json:"name,omitempty"` - StaticPatterns *[]string `json:"static_patterns,omitempty"` - VerifyJwt *bool `json:"verify_jwt,omitempty"` - } `json:"metadata"` -} - -// FunctionResponseOutput defines model for FunctionResponse_Output. -type FunctionResponseOutput struct { - CreatedAt int64 `json:"created_at"` - EntrypointPath *string `json:"entrypoint_path,omitempty"` - EzbrSha256 *string `json:"ezbr_sha256,omitempty"` - Id string `json:"id"` - ImportMap *bool `json:"import_map,omitempty"` - ImportMapPath *string `json:"import_map_path,omitempty"` - Name string `json:"name"` - Slug string `json:"slug"` - Status FunctionResponseOutputStatus `json:"status"` - UpdatedAt int64 `json:"updated_at"` - VerifyJwt *bool `json:"verify_jwt,omitempty"` - Version int `json:"version"` -} - -// FunctionResponseOutputStatus defines model for FunctionResponseOutput.Status. -type FunctionResponseOutputStatus string - -// FunctionSlugResponseOutput defines model for FunctionSlugResponse_Output. -type FunctionSlugResponseOutput struct { - CreatedAt int64 `json:"created_at"` - EntrypointPath *string `json:"entrypoint_path,omitempty"` - EzbrSha256 *string `json:"ezbr_sha256,omitempty"` - Id string `json:"id"` - ImportMap *bool `json:"import_map,omitempty"` - ImportMapPath *string `json:"import_map_path,omitempty"` - Name string `json:"name"` - Slug string `json:"slug"` - Status FunctionSlugResponseOutputStatus `json:"status"` - UpdatedAt int64 `json:"updated_at"` - VerifyJwt *bool `json:"verify_jwt,omitempty"` - Version int `json:"version"` -} - -// FunctionSlugResponseOutputStatus defines model for FunctionSlugResponseOutput.Status. -type FunctionSlugResponseOutputStatus string - -// GetProjectAvailableRestoreVersionsResponseOutput defines model for GetProjectAvailableRestoreVersionsResponse_Output. -type GetProjectAvailableRestoreVersionsResponseOutput struct { - AvailableVersions []struct { - PostgresEngine GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsPostgresEngine `json:"postgres_engine"` - ReleaseChannel GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannel `json:"release_channel"` - Version string `json:"version"` - } `json:"available_versions"` -} - -// GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsPostgresEngine defines model for GetProjectAvailableRestoreVersionsResponseOutput.AvailableVersions.PostgresEngine. -type GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsPostgresEngine string - -// GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannel defines model for GetProjectAvailableRestoreVersionsResponseOutput.AvailableVersions.ReleaseChannel. -type GetProjectAvailableRestoreVersionsResponseOutputAvailableVersionsReleaseChannel string - -// GetProjectDbMetadataResponseOutput defines model for GetProjectDbMetadataResponse_Output. -type GetProjectDbMetadataResponseOutput struct { - Databases []struct { - Name string `json:"name"` - Schemas []struct { - Name string `json:"name"` - } `json:"schemas"` - } `json:"databases"` -} - -// GetProviderResponseOutput defines model for GetProviderResponse_Output. -type GetProviderResponseOutput struct { - CreatedAt *string `json:"created_at,omitempty"` - Domains *[]struct { - CreatedAt *string `json:"created_at,omitempty"` - Domain *string `json:"domain,omitempty"` - UpdatedAt *string `json:"updated_at,omitempty"` - } `json:"domains,omitempty"` - Id string `json:"id"` - Saml *struct { - AttributeMapping *struct { - Keys map[string]struct { - Array *bool `json:"array,omitempty"` - Default interface{} `json:"default,omitempty"` - Name *string `json:"name,omitempty"` - Names *[]string `json:"names,omitempty"` - } `json:"keys"` - } `json:"attribute_mapping,omitempty"` - EntityId string `json:"entity_id"` - MetadataUrl *string `json:"metadata_url,omitempty"` - MetadataXml *string `json:"metadata_xml,omitempty"` - NameIdFormat *string `json:"name_id_format,omitempty"` - } `json:"saml,omitempty"` - UpdatedAt *string `json:"updated_at,omitempty"` -} - -// InviteExternalUserJitAccessBody defines model for InviteExternalUserJitAccessBody. -type InviteExternalUserJitAccessBody struct { - Email openapi_types.Email `json:"email"` - Roles []struct { - AllowedNetworks *struct { - AllowedCidrs *[]struct { - Cidr string `json:"cidr"` - } `json:"allowed_cidrs,omitempty"` - AllowedCidrsV6 *[]struct { - Cidr string `json:"cidr"` - } `json:"allowed_cidrs_v6,omitempty"` - } `json:"allowed_networks,omitempty"` - BranchesOnly *bool `json:"branches_only,omitempty"` - ExpiresAt *float32 `json:"expires_at,omitempty"` - Role string `json:"role"` - } `json:"roles"` -} - -// InviteExternalUserJitResponseOutput defines model for InviteExternalUserJitResponse_Output. -type InviteExternalUserJitResponseOutput struct { - Email openapi_types.Email `json:"email"` - InviteId openapi_types.UUID `json:"invite_id"` - UserRoles []struct { - AllowedNetworks *struct { - AllowedCidrs *[]struct { - Cidr string `json:"cidr"` - } `json:"allowed_cidrs,omitempty"` - AllowedCidrsV6 *[]struct { - Cidr string `json:"cidr"` - } `json:"allowed_cidrs_v6,omitempty"` - } `json:"allowed_networks,omitempty"` - BranchesOnly *bool `json:"branches_only,omitempty"` - ExpiresAt *float32 `json:"expires_at,omitempty"` - Role string `json:"role"` - } `json:"user_roles"` -} - -// JitAccessRequestRequest defines model for JitAccessRequestRequest. -type JitAccessRequestRequest struct { - State JitAccessRequestRequestState `json:"state"` -} - -// JitAccessRequestRequestState defines model for JitAccessRequestRequest.State. -type JitAccessRequestRequestState string - -// JitAccessResponseOutput defines model for JitAccessResponse_Output. -type JitAccessResponseOutput struct { - UserId *openapi_types.UUID `json:"user_id,omitempty"` - UserRoles []struct { - AllowedNetworks *struct { - AllowedCidrs *[]struct { - Cidr string `json:"cidr"` - } `json:"allowed_cidrs,omitempty"` - AllowedCidrsV6 *[]struct { - Cidr string `json:"cidr"` - } `json:"allowed_cidrs_v6,omitempty"` - } `json:"allowed_networks,omitempty"` - BranchesOnly *bool `json:"branches_only,omitempty"` - ExpiresAt *float32 `json:"expires_at,omitempty"` - Role string `json:"role"` - } `json:"user_roles"` -} - -// JitAuthorizeAccessResponseOutput defines model for JitAuthorizeAccessResponse_Output. -type JitAuthorizeAccessResponseOutput struct { - Act *string `json:"act,omitempty"` - UserId *openapi_types.UUID `json:"user_id,omitempty"` - UserRole struct { - AllowedNetworks *struct { - AllowedCidrs *[]struct { - Cidr string `json:"cidr"` - } `json:"allowed_cidrs,omitempty"` - AllowedCidrsV6 *[]struct { - Cidr string `json:"cidr"` - } `json:"allowed_cidrs_v6,omitempty"` - } `json:"allowed_networks,omitempty"` - BranchesOnly *bool `json:"branches_only,omitempty"` - ExpiresAt *float32 `json:"expires_at,omitempty"` - Role string `json:"role"` - } `json:"user_role"` -} - -// JitListAccessResponseOutput defines model for JitListAccessResponse_Output. -type JitListAccessResponseOutput struct { - Items []JitListAccessResponseOutput_Items_Item `json:"items"` -} - -// JitListAccessResponseOutputItems0 defines model for . -type JitListAccessResponseOutputItems0 struct { - ExpiresAt nullable.Nullable[string] `json:"expires_at"` - InviteId nullable.Nullable[openapi_types.UUID] `json:"invite_id"` - PrimaryEmail nullable.Nullable[string] `json:"primary_email"` - UserId openapi_types.UUID `json:"user_id"` - UserRoles []struct { - AllowedNetworks *struct { - AllowedCidrs *[]struct { - Cidr string `json:"cidr"` - } `json:"allowed_cidrs,omitempty"` - AllowedCidrsV6 *[]struct { - Cidr string `json:"cidr"` - } `json:"allowed_cidrs_v6,omitempty"` - } `json:"allowed_networks,omitempty"` - BranchesOnly *bool `json:"branches_only,omitempty"` - ExpiresAt *float32 `json:"expires_at,omitempty"` - Role string `json:"role"` - } `json:"user_roles"` -} - -// JitListAccessResponseOutputItems1 defines model for . -type JitListAccessResponseOutputItems1 struct { - ExpiresAt string `json:"expires_at"` - InviteId openapi_types.UUID `json:"invite_id"` - PrimaryEmail string `json:"primary_email"` - UserId nullable.Nullable[openapi_types.UUID] `json:"user_id"` - UserRoles []struct { - AllowedNetworks *struct { - AllowedCidrs *[]struct { - Cidr string `json:"cidr"` - } `json:"allowed_cidrs,omitempty"` - AllowedCidrsV6 *[]struct { - Cidr string `json:"cidr"` - } `json:"allowed_cidrs_v6,omitempty"` - } `json:"allowed_networks,omitempty"` - BranchesOnly *bool `json:"branches_only,omitempty"` - ExpiresAt *float32 `json:"expires_at,omitempty"` - Role string `json:"role"` - } `json:"user_roles"` -} - -// JitListAccessResponseOutput_Items_Item defines model for JitListAccessResponse_Output.items.Item. -type JitListAccessResponseOutput_Items_Item struct { - union json.RawMessage -} - -// JsonValueOutput Any JSON-serializable value -type JsonValueOutput struct { - union json.RawMessage -} - -// JsonValueOutput0 defines model for . -type JsonValueOutput0 struct { - union json.RawMessage -} - -// JsonValueOutput00 defines model for . -type JsonValueOutput00 = string - -// JsonValueOutput01 defines model for . -type JsonValueOutput01 = float32 - -// JsonValueOutput02 defines model for . -type JsonValueOutput02 = bool - -// JsonValueOutput1 defines model for . -type JsonValueOutput1 = []JsonValueOutput - -// JsonValueOutput2 defines model for . -type JsonValueOutput2 map[string]JsonValueOutput - -// LegacyApiKeysResponseOutput defines model for LegacyApiKeysResponse_Output. -type LegacyApiKeysResponseOutput struct { - Enabled bool `json:"enabled"` -} - -// ListActionRunResponseOutput defines model for ListActionRunResponse_Output. -type ListActionRunResponseOutput = []struct { - BranchId string `json:"branch_id"` - CheckRunId nullable.Nullable[float32] `json:"check_run_id"` - CreatedAt string `json:"created_at"` - GitConfig nullable.Nullable[interface{}] `json:"git_config,omitempty"` - Id string `json:"id"` - RunSteps []struct { - CreatedAt string `json:"created_at"` - Name ListActionRunResponseOutputRunStepsName `json:"name"` - Status ListActionRunResponseOutputRunStepsStatus `json:"status"` - UpdatedAt string `json:"updated_at"` - } `json:"run_steps"` - UpdatedAt string `json:"updated_at"` - Workdir nullable.Nullable[string] `json:"workdir"` -} - -// ListActionRunResponseOutputRunStepsName defines model for ListActionRunResponseOutput.RunSteps.Name. -type ListActionRunResponseOutputRunStepsName string - -// ListActionRunResponseOutputRunStepsStatus defines model for ListActionRunResponseOutput.RunSteps.Status. -type ListActionRunResponseOutputRunStepsStatus string - -// ListProjectAddonsResponseOutput defines model for ListProjectAddonsResponse_Output. -type ListProjectAddonsResponseOutput struct { - AvailableAddons []struct { - Name string `json:"name"` - Type ListProjectAddonsResponseOutputAvailableAddonsType `json:"type"` - Variants []struct { - Id ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id `json:"id"` - - // Meta Any JSON-serializable value - Meta *JsonValueOutput `json:"meta,omitempty"` - Name string `json:"name"` - Price struct { - Amount float32 `json:"amount"` - Description string `json:"description"` - Interval ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceInterval `json:"interval"` - Type ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceType `json:"type"` - } `json:"price"` - } `json:"variants"` - } `json:"available_addons"` - SelectedAddons []struct { - Type ListProjectAddonsResponseOutputSelectedAddonsType `json:"type"` - Variant struct { - Id ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id `json:"id"` - - // Meta Any JSON-serializable value - Meta *JsonValueOutput `json:"meta,omitempty"` - Name string `json:"name"` - Price struct { - Amount float32 `json:"amount"` - Description string `json:"description"` - Interval ListProjectAddonsResponseOutputSelectedAddonsVariantPriceInterval `json:"interval"` - Type ListProjectAddonsResponseOutputSelectedAddonsVariantPriceType `json:"type"` - } `json:"price"` - } `json:"variant"` - } `json:"selected_addons"` -} - -// ListProjectAddonsResponseOutputAvailableAddonsType defines model for ListProjectAddonsResponseOutput.AvailableAddons.Type. -type ListProjectAddonsResponseOutputAvailableAddonsType string - -// ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 defines model for ListProjectAddonsResponseOutput.AvailableAddons.Variants.Id.0. -type ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 string - -// ListProjectAddonsResponseOutputAvailableAddonsVariantsId1 defines model for ListProjectAddonsResponseOutput.AvailableAddons.Variants.Id.1. -type ListProjectAddonsResponseOutputAvailableAddonsVariantsId1 string - -// ListProjectAddonsResponseOutputAvailableAddonsVariantsId2 defines model for ListProjectAddonsResponseOutput.AvailableAddons.Variants.Id.2. -type ListProjectAddonsResponseOutputAvailableAddonsVariantsId2 string - -// ListProjectAddonsResponseOutputAvailableAddonsVariantsId3 defines model for ListProjectAddonsResponseOutput.AvailableAddons.Variants.Id.3. -type ListProjectAddonsResponseOutputAvailableAddonsVariantsId3 string - -// ListProjectAddonsResponseOutputAvailableAddonsVariantsId4 defines model for ListProjectAddonsResponseOutput.AvailableAddons.Variants.Id.4. -type ListProjectAddonsResponseOutputAvailableAddonsVariantsId4 string - -// ListProjectAddonsResponseOutputAvailableAddonsVariantsId5 defines model for ListProjectAddonsResponseOutput.AvailableAddons.Variants.Id.5. -type ListProjectAddonsResponseOutputAvailableAddonsVariantsId5 string - -// ListProjectAddonsResponseOutputAvailableAddonsVariantsId6 defines model for ListProjectAddonsResponseOutput.AvailableAddons.Variants.Id.6. -type ListProjectAddonsResponseOutputAvailableAddonsVariantsId6 string - -// ListProjectAddonsResponseOutputAvailableAddonsVariantsId7 defines model for ListProjectAddonsResponseOutput.AvailableAddons.Variants.Id.7. -type ListProjectAddonsResponseOutputAvailableAddonsVariantsId7 string - -// ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id defines model for ListProjectAddonsResponseOutput.AvailableAddons.Variants.Id. -type ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id struct { - union json.RawMessage -} - -// ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceInterval defines model for ListProjectAddonsResponseOutput.AvailableAddons.Variants.Price.Interval. -type ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceInterval string - -// ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceType defines model for ListProjectAddonsResponseOutput.AvailableAddons.Variants.Price.Type. -type ListProjectAddonsResponseOutputAvailableAddonsVariantsPriceType string - -// ListProjectAddonsResponseOutputSelectedAddonsType defines model for ListProjectAddonsResponseOutput.SelectedAddons.Type. -type ListProjectAddonsResponseOutputSelectedAddonsType string - -// ListProjectAddonsResponseOutputSelectedAddonsVariantId0 defines model for ListProjectAddonsResponseOutput.SelectedAddons.Variant.Id.0. -type ListProjectAddonsResponseOutputSelectedAddonsVariantId0 string - -// ListProjectAddonsResponseOutputSelectedAddonsVariantId1 defines model for ListProjectAddonsResponseOutput.SelectedAddons.Variant.Id.1. -type ListProjectAddonsResponseOutputSelectedAddonsVariantId1 string - -// ListProjectAddonsResponseOutputSelectedAddonsVariantId2 defines model for ListProjectAddonsResponseOutput.SelectedAddons.Variant.Id.2. -type ListProjectAddonsResponseOutputSelectedAddonsVariantId2 string - -// ListProjectAddonsResponseOutputSelectedAddonsVariantId3 defines model for ListProjectAddonsResponseOutput.SelectedAddons.Variant.Id.3. -type ListProjectAddonsResponseOutputSelectedAddonsVariantId3 string - -// ListProjectAddonsResponseOutputSelectedAddonsVariantId4 defines model for ListProjectAddonsResponseOutput.SelectedAddons.Variant.Id.4. -type ListProjectAddonsResponseOutputSelectedAddonsVariantId4 string - -// ListProjectAddonsResponseOutputSelectedAddonsVariantId5 defines model for ListProjectAddonsResponseOutput.SelectedAddons.Variant.Id.5. -type ListProjectAddonsResponseOutputSelectedAddonsVariantId5 string - -// ListProjectAddonsResponseOutputSelectedAddonsVariantId6 defines model for ListProjectAddonsResponseOutput.SelectedAddons.Variant.Id.6. -type ListProjectAddonsResponseOutputSelectedAddonsVariantId6 string - -// ListProjectAddonsResponseOutputSelectedAddonsVariantId7 defines model for ListProjectAddonsResponseOutput.SelectedAddons.Variant.Id.7. -type ListProjectAddonsResponseOutputSelectedAddonsVariantId7 string - -// ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id defines model for ListProjectAddonsResponseOutput.SelectedAddons.Variant.Id. -type ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id struct { - union json.RawMessage -} - -// ListProjectAddonsResponseOutputSelectedAddonsVariantPriceInterval defines model for ListProjectAddonsResponseOutput.SelectedAddons.Variant.Price.Interval. -type ListProjectAddonsResponseOutputSelectedAddonsVariantPriceInterval string - -// ListProjectAddonsResponseOutputSelectedAddonsVariantPriceType defines model for ListProjectAddonsResponseOutput.SelectedAddons.Variant.Price.Type. -type ListProjectAddonsResponseOutputSelectedAddonsVariantPriceType string - -// ListProvidersResponseOutput defines model for ListProvidersResponse_Output. -type ListProvidersResponseOutput struct { - Items []struct { - CreatedAt *string `json:"created_at,omitempty"` - Domains *[]struct { - CreatedAt *string `json:"created_at,omitempty"` - Domain *string `json:"domain,omitempty"` - UpdatedAt *string `json:"updated_at,omitempty"` - } `json:"domains,omitempty"` - Id string `json:"id"` - Saml *struct { - AttributeMapping *struct { - Keys map[string]struct { - Array *bool `json:"array,omitempty"` - Default interface{} `json:"default,omitempty"` - Name *string `json:"name,omitempty"` - Names *[]string `json:"names,omitempty"` - } `json:"keys"` - } `json:"attribute_mapping,omitempty"` - EntityId string `json:"entity_id"` - MetadataUrl *string `json:"metadata_url,omitempty"` - MetadataXml *string `json:"metadata_xml,omitempty"` - NameIdFormat *string `json:"name_id_format,omitempty"` - } `json:"saml,omitempty"` - UpdatedAt *string `json:"updated_at,omitempty"` - } `json:"items"` -} - -// NetworkBanResponseEnrichedOutput defines model for NetworkBanResponseEnriched_Output. -type NetworkBanResponseEnrichedOutput struct { - BannedIpv4Addresses []struct { - BannedAddress string `json:"banned_address"` - Identifier string `json:"identifier"` - Type string `json:"type"` - } `json:"banned_ipv4_addresses"` -} - -// NetworkBanResponseOutput defines model for NetworkBanResponse_Output. -type NetworkBanResponseOutput struct { - BannedIpv4Addresses []string `json:"banned_ipv4_addresses"` -} - -// NetworkRestrictionsPatchRequest defines model for NetworkRestrictionsPatchRequest. -type NetworkRestrictionsPatchRequest struct { - Add *struct { - DbAllowedCidrs *[]string `json:"dbAllowedCidrs,omitempty"` - DbAllowedCidrsV6 *[]string `json:"dbAllowedCidrsV6,omitempty"` - } `json:"add,omitempty"` - Remove *struct { - DbAllowedCidrs *[]string `json:"dbAllowedCidrs,omitempty"` - DbAllowedCidrsV6 *[]string `json:"dbAllowedCidrsV6,omitempty"` - } `json:"remove,omitempty"` -} - -// NetworkRestrictionsRequest defines model for NetworkRestrictionsRequest. -type NetworkRestrictionsRequest struct { - DbAllowedCidrs *[]string `json:"dbAllowedCidrs,omitempty"` - DbAllowedCidrsV6 *[]string `json:"dbAllowedCidrsV6,omitempty"` -} - -// NetworkRestrictionsResponseOutput defines model for NetworkRestrictionsResponse_Output. -type NetworkRestrictionsResponseOutput struct { - AppliedAt *time.Time `json:"applied_at,omitempty"` - - // Config At any given point in time, this is the config that the user has requested be applied to their project. The `status` field indicates if it has been applied to the project, or is pending. When an updated config is received, the applied config is moved to `old_config`. - Config struct { - DbAllowedCidrs *[]string `json:"dbAllowedCidrs,omitempty"` - DbAllowedCidrsV6 *[]string `json:"dbAllowedCidrsV6,omitempty"` - } `json:"config"` - Entitlement NetworkRestrictionsResponseOutputEntitlement `json:"entitlement"` - - // OldConfig Populated when a new config has been received, but not registered as successfully applied to a project. - OldConfig *struct { - DbAllowedCidrs *[]string `json:"dbAllowedCidrs,omitempty"` - DbAllowedCidrsV6 *[]string `json:"dbAllowedCidrsV6,omitempty"` - } `json:"old_config,omitempty"` - Status NetworkRestrictionsResponseOutputStatus `json:"status"` - UpdatedAt *time.Time `json:"updated_at,omitempty"` -} - -// NetworkRestrictionsResponseOutputEntitlement defines model for NetworkRestrictionsResponseOutput.Entitlement. -type NetworkRestrictionsResponseOutputEntitlement string - -// NetworkRestrictionsResponseOutputStatus defines model for NetworkRestrictionsResponseOutput.Status. -type NetworkRestrictionsResponseOutputStatus string - -// NetworkRestrictionsV2ResponseOutput defines model for NetworkRestrictionsV2Response_Output. -type NetworkRestrictionsV2ResponseOutput struct { - AppliedAt *time.Time `json:"applied_at,omitempty"` - - // Config At any given point in time, this is the config that the user has requested be applied to their project. The `status` field indicates if it has been applied to the project, or is pending. When an updated config is received, the applied config is moved to `old_config`. - Config struct { - DbAllowedCidrs *[]struct { - Address string `json:"address"` - Type NetworkRestrictionsV2ResponseOutputConfigDbAllowedCidrsType `json:"type"` - } `json:"dbAllowedCidrs,omitempty"` - } `json:"config"` - Entitlement NetworkRestrictionsV2ResponseOutputEntitlement `json:"entitlement"` - - // OldConfig Populated when a new config has been received, but not registered as successfully applied to a project. - OldConfig *struct { - DbAllowedCidrs *[]struct { - Address string `json:"address"` - Type NetworkRestrictionsV2ResponseOutputOldConfigDbAllowedCidrsType `json:"type"` - } `json:"dbAllowedCidrs,omitempty"` - } `json:"old_config,omitempty"` - Status NetworkRestrictionsV2ResponseOutputStatus `json:"status"` - UpdatedAt *time.Time `json:"updated_at,omitempty"` -} - -// NetworkRestrictionsV2ResponseOutputConfigDbAllowedCidrsType defines model for NetworkRestrictionsV2ResponseOutput.Config.DbAllowedCidrs.Type. -type NetworkRestrictionsV2ResponseOutputConfigDbAllowedCidrsType string - -// NetworkRestrictionsV2ResponseOutputEntitlement defines model for NetworkRestrictionsV2ResponseOutput.Entitlement. -type NetworkRestrictionsV2ResponseOutputEntitlement string - -// NetworkRestrictionsV2ResponseOutputOldConfigDbAllowedCidrsType defines model for NetworkRestrictionsV2ResponseOutput.OldConfig.DbAllowedCidrs.Type. -type NetworkRestrictionsV2ResponseOutputOldConfigDbAllowedCidrsType string - -// NetworkRestrictionsV2ResponseOutputStatus defines model for NetworkRestrictionsV2ResponseOutput.Status. -type NetworkRestrictionsV2ResponseOutputStatus string - -// OAuthRevokeTokenBody defines model for OAuthRevokeTokenBody. -type OAuthRevokeTokenBody struct { - ClientId openapi_types.UUID `json:"client_id"` - ClientSecret string `json:"client_secret"` - RefreshToken string `json:"refresh_token"` -} - -// OAuthTokenBody defines model for OAuthTokenBody. -type OAuthTokenBody struct { - // Assertion IDJAG assertion JWT for grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer. Beta - available on Team and Enterprise plans only. - Assertion *string `json:"assertion,omitempty"` - ClientId *openapi_types.UUID `json:"client_id,omitempty"` - ClientSecret *string `json:"client_secret,omitempty"` - Code *string `json:"code,omitempty"` - CodeVerifier *string `json:"code_verifier,omitempty"` - GrantType *OAuthTokenBodyGrantType `json:"grant_type,omitempty"` - RedirectUri *string `json:"redirect_uri,omitempty"` - RefreshToken *string `json:"refresh_token,omitempty"` - - // Resource Resource indicator for MCP (Model Context Protocol) clients - Resource *string `json:"resource,omitempty"` - Scope *string `json:"scope,omitempty"` -} - -// OAuthTokenBodyGrantType defines model for OAuthTokenBody.GrantType. -type OAuthTokenBodyGrantType string - -// OAuthTokenResponseOutput defines model for OAuthTokenResponse_Output. -type OAuthTokenResponseOutput struct { - AccessToken string `json:"access_token"` - ExpiresIn int `json:"expires_in"` - - // RefreshToken The `urn:ietf:params:oauth:grant-type:jwt-bearer` grant type issues access tokens only, no refresh token is returned and the token cannot be revoked via `/v1/oauth/revoke`. - RefreshToken *string `json:"refresh_token,omitempty"` - TokenType OAuthTokenResponseOutputTokenType `json:"token_type"` -} - -// OAuthTokenResponseOutputTokenType defines model for OAuthTokenResponseOutput.TokenType. -type OAuthTokenResponseOutputTokenType string - -// OrganizationProjectClaimResponseOutput defines model for OrganizationProjectClaimResponse_Output. -type OrganizationProjectClaimResponseOutput struct { - CreatedAt string `json:"created_at"` - CreatedBy openapi_types.UUID `json:"created_by"` - ExpiresAt string `json:"expires_at"` - Preview struct { - Errors []struct { - Key string `json:"key"` - Message string `json:"message"` - } `json:"errors"` - Info []struct { - Key string `json:"key"` - Message string `json:"message"` - } `json:"info"` - MembersExceedingFreeProjectLimit []struct { - Limit float32 `json:"limit"` - Name string `json:"name"` - } `json:"members_exceeding_free_project_limit"` - SourceSubscriptionPlan OrganizationProjectClaimResponseOutputPreviewSourceSubscriptionPlan `json:"source_subscription_plan"` - TargetSubscriptionPlan nullable.Nullable[OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlan] `json:"target_subscription_plan"` - Valid bool `json:"valid"` - Warnings []struct { - Key string `json:"key"` - Message string `json:"message"` - } `json:"warnings"` - } `json:"preview"` - Project struct { - Name string `json:"name"` - Ref string `json:"ref"` - } `json:"project"` -} - -// OrganizationProjectClaimResponseOutputPreviewSourceSubscriptionPlan defines model for OrganizationProjectClaimResponseOutput.Preview.SourceSubscriptionPlan. -type OrganizationProjectClaimResponseOutputPreviewSourceSubscriptionPlan string - -// OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlan defines model for OrganizationProjectClaimResponseOutput.Preview.TargetSubscriptionPlan. -type OrganizationProjectClaimResponseOutputPreviewTargetSubscriptionPlan string - -// OrganizationProjectsResponseOutput defines model for OrganizationProjectsResponse_Output. -type OrganizationProjectsResponseOutput struct { - Pagination struct { - // Count Total number of projects. Use this to calculate the total number of pages. - Count float32 `json:"count"` - - // Limit Maximum number of projects per page - Limit float32 `json:"limit"` - - // Offset Number of projects skipped in this response - Offset float32 `json:"offset"` - } `json:"pagination"` - Projects []struct { - CloudProvider string `json:"cloud_provider"` - Databases []struct { - CloudProvider string `json:"cloud_provider"` - DiskLastModifiedAt *string `json:"disk_last_modified_at,omitempty"` - DiskThroughputMbps *float32 `json:"disk_throughput_mbps,omitempty"` - DiskType *OrganizationProjectsResponseOutputProjectsDatabasesDiskType `json:"disk_type,omitempty"` - DiskVolumeSizeGb *float32 `json:"disk_volume_size_gb,omitempty"` - Identifier string `json:"identifier"` - InfraComputeSize *OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize `json:"infra_compute_size,omitempty"` - Region string `json:"region"` - Status OrganizationProjectsResponseOutputProjectsDatabasesStatus `json:"status"` - Type OrganizationProjectsResponseOutputProjectsDatabasesType `json:"type"` - } `json:"databases"` - InsertedAt string `json:"inserted_at"` - IsBranch bool `json:"is_branch"` - Name string `json:"name"` - Ref string `json:"ref"` - Region string `json:"region"` - Status OrganizationProjectsResponseOutputProjectsStatus `json:"status"` - } `json:"projects"` -} - -// OrganizationProjectsResponseOutputProjectsDatabasesDiskType defines model for OrganizationProjectsResponseOutput.Projects.Databases.DiskType. -type OrganizationProjectsResponseOutputProjectsDatabasesDiskType string - -// OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize defines model for OrganizationProjectsResponseOutput.Projects.Databases.InfraComputeSize. -type OrganizationProjectsResponseOutputProjectsDatabasesInfraComputeSize string - -// OrganizationProjectsResponseOutputProjectsDatabasesStatus defines model for OrganizationProjectsResponseOutput.Projects.Databases.Status. -type OrganizationProjectsResponseOutputProjectsDatabasesStatus string - -// OrganizationProjectsResponseOutputProjectsDatabasesType defines model for OrganizationProjectsResponseOutput.Projects.Databases.Type. -type OrganizationProjectsResponseOutputProjectsDatabasesType string - -// OrganizationProjectsResponseOutputProjectsStatus defines model for OrganizationProjectsResponseOutput.Projects.Status. -type OrganizationProjectsResponseOutputProjectsStatus string - -// OrganizationResponseV1Output defines model for OrganizationResponseV1_Output. -type OrganizationResponseV1Output struct { - // Id Deprecated: Use `slug` instead. - // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set - Id string `json:"id"` - Name string `json:"name"` - - // Slug Organization slug - Slug string `json:"slug"` -} - -// PgsodiumConfigResponseOutput defines model for PgsodiumConfigResponse_Output. -type PgsodiumConfigResponseOutput struct { - // RootKey The pgsodium root key: 32 bytes, hex-encoded (64 characters). - RootKey string `json:"root_key"` -} - -// PlanGateErrorBody defines model for PlanGateErrorBody. -type PlanGateErrorBody struct { - // Error Present on entitlement denials. Other errors with this status code (validation, billing state) carry only message. - Error *struct { - // Code Machine-readable marker for plan-gated denials - Code PlanGateErrorBodyErrorCode `json:"code"` - - // Feature Entitlement feature key that failed the check - Feature string `json:"feature"` - - // UpgradeUrl Billing page URL for the organization, present when the org is resolvable - UpgradeUrl *string `json:"upgrade_url,omitempty"` - } `json:"error,omitempty"` - - // Message Human-readable explanation of the plan gate - Message string `json:"message"` -} - -// PlanGateErrorBodyErrorCode Machine-readable marker for plan-gated denials -type PlanGateErrorBodyErrorCode string - -// PostgresConfigResponseOutput defines model for PostgresConfigResponse_Output. -type PostgresConfigResponseOutput struct { - AutovacuumMaxWorkers *int `json:"autovacuum_max_workers,omitempty"` - AutovacuumWorkMem *string `json:"autovacuum_work_mem,omitempty"` - - // CheckpointTimeout Default unit: s - CheckpointTimeout *string `json:"checkpoint_timeout,omitempty"` - CronLogStatement *bool `json:"cron.log_statement,omitempty"` - EffectiveCacheSize *string `json:"effective_cache_size,omitempty"` - HotStandbyFeedback *bool `json:"hot_standby_feedback,omitempty"` - - // LogAutovacuumMinDuration Default unit: ms - LogAutovacuumMinDuration *string `json:"log_autovacuum_min_duration,omitempty"` - LogCheckpoints *bool `json:"log_checkpoints,omitempty"` - LogConnections *bool `json:"log_connections,omitempty"` - LogDisconnections *bool `json:"log_disconnections,omitempty"` - LogDuration *bool `json:"log_duration,omitempty"` - LogLockWaits *bool `json:"log_lock_waits,omitempty"` - LogRecoveryConflictWaits *bool `json:"log_recovery_conflict_waits,omitempty"` - LogReplicationCommands *bool `json:"log_replication_commands,omitempty"` - - // LogStartupProgressInterval Default unit: ms - LogStartupProgressInterval *string `json:"log_startup_progress_interval,omitempty"` - LogTempFiles *string `json:"log_temp_files,omitempty"` - LogicalDecodingWorkMem *string `json:"logical_decoding_work_mem,omitempty"` - MaintenanceWorkMem *string `json:"maintenance_work_mem,omitempty"` - MaxConnections *int `json:"max_connections,omitempty"` - MaxLocksPerTransaction *int `json:"max_locks_per_transaction,omitempty"` - MaxLogicalReplicationWorkers *int `json:"max_logical_replication_workers,omitempty"` - MaxParallelMaintenanceWorkers *int `json:"max_parallel_maintenance_workers,omitempty"` - MaxParallelWorkers *int `json:"max_parallel_workers,omitempty"` - MaxParallelWorkersPerGather *int `json:"max_parallel_workers_per_gather,omitempty"` - MaxReplicationSlots *int `json:"max_replication_slots,omitempty"` - MaxSlotWalKeepSize *string `json:"max_slot_wal_keep_size,omitempty"` - MaxStandbyArchiveDelay *string `json:"max_standby_archive_delay,omitempty"` - MaxStandbyStreamingDelay *string `json:"max_standby_streaming_delay,omitempty"` - MaxSyncWorkersPerSubscription *int `json:"max_sync_workers_per_subscription,omitempty"` - MaxWalSenders *int `json:"max_wal_senders,omitempty"` - MaxWalSize *string `json:"max_wal_size,omitempty"` - MaxWorkerProcesses *int `json:"max_worker_processes,omitempty"` - PgNetBatchSize *int `json:"pg_net.batch_size,omitempty"` - PgStatStatementsMax *int `json:"pg_stat_statements.max,omitempty"` - SessionReplicationRole *PostgresConfigResponseOutputSessionReplicationRole `json:"session_replication_role,omitempty"` - SharedBuffers *string `json:"shared_buffers,omitempty"` - - // StatementTimeout Default unit: ms - StatementTimeout *string `json:"statement_timeout,omitempty"` - TrackActivityQuerySize *string `json:"track_activity_query_size,omitempty"` - TrackCommitTimestamp *bool `json:"track_commit_timestamp,omitempty"` - WalKeepSize *string `json:"wal_keep_size,omitempty"` - - // WalSenderTimeout Default unit: ms - WalSenderTimeout *string `json:"wal_sender_timeout,omitempty"` - WorkMem *string `json:"work_mem,omitempty"` -} - -// PostgresConfigResponseOutputSessionReplicationRole defines model for PostgresConfigResponseOutput.SessionReplicationRole. -type PostgresConfigResponseOutputSessionReplicationRole string - -// PostgrestConfigWithJWTSecretResponseOutput defines model for PostgrestConfigWithJWTSecretResponse_Output. -type PostgrestConfigWithJWTSecretResponseOutput struct { - DbExtraSearchPath string `json:"db_extra_search_path"` - - // DbPool If `null`, the value is automatically configured based on compute size. - DbPool nullable.Nullable[int] `json:"db_pool"` - - // DbPoolAcquisitionTimeout If `null`, the value is automatically configured to 10. - DbPoolAcquisitionTimeout nullable.Nullable[int] `json:"db_pool_acquisition_timeout"` - - // DbPoolMaxIdletime If `null`, the value is automatically configured to 30. - DbPoolMaxIdletime nullable.Nullable[int] `json:"db_pool_max_idletime"` - - // DbPoolMaxLifetime If `null`, the value is automatically configured to 1800. - DbPoolMaxLifetime nullable.Nullable[int] `json:"db_pool_max_lifetime"` - DbSchema string `json:"db_schema"` - JwtSecret *string `json:"jwt_secret,omitempty"` - MaxRows int `json:"max_rows"` -} - -// ProjectClaimTokenResponseOutput defines model for ProjectClaimTokenResponse_Output. -type ProjectClaimTokenResponseOutput struct { - CreatedAt string `json:"created_at"` - CreatedBy openapi_types.UUID `json:"created_by"` - ExpiresAt string `json:"expires_at"` - TokenAlias string `json:"token_alias"` -} - -// ProjectUpgradeEligibilityResponseOutput defines model for ProjectUpgradeEligibilityResponse_Output. -type ProjectUpgradeEligibilityResponseOutput struct { - CurrentAppVersion string `json:"current_app_version"` - CurrentAppVersionReleaseChannel ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannel `json:"current_app_version_release_channel"` - DurationEstimateHours float32 `json:"duration_estimate_hours"` - Eligible bool `json:"eligible"` - LatestAppVersion string `json:"latest_app_version"` - LegacyAuthCustomRoles []string `json:"legacy_auth_custom_roles"` - - // ObjectsToBeDropped Use validation_errors instead. - // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set - ObjectsToBeDropped []string `json:"objects_to_be_dropped"` - TargetUpgradeVersions []struct { - AppVersion string `json:"app_version"` - PostgresVersion ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsPostgresVersion `json:"postgres_version"` - ReleaseChannel ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannel `json:"release_channel"` - } `json:"target_upgrade_versions"` - - // UnsupportedExtensions Use validation_errors instead. - // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set - UnsupportedExtensions []string `json:"unsupported_extensions"` - - // UserDefinedObjectsInInternalSchemas Use validation_errors instead. - // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set - UserDefinedObjectsInInternalSchemas []string `json:"user_defined_objects_in_internal_schemas"` - ValidationErrors []ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item `json:"validation_errors"` - Warnings []ProjectUpgradeEligibilityResponseOutput_Warnings_Item `json:"warnings"` -} - -// ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannel defines model for ProjectUpgradeEligibilityResponseOutput.CurrentAppVersionReleaseChannel. -type ProjectUpgradeEligibilityResponseOutputCurrentAppVersionReleaseChannel string - -// ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsPostgresVersion defines model for ProjectUpgradeEligibilityResponseOutput.TargetUpgradeVersions.PostgresVersion. -type ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsPostgresVersion string - -// ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannel defines model for ProjectUpgradeEligibilityResponseOutput.TargetUpgradeVersions.ReleaseChannel. -type ProjectUpgradeEligibilityResponseOutputTargetUpgradeVersionsReleaseChannel string - -// ProjectUpgradeEligibilityResponseOutputValidationErrors0 defines model for . -type ProjectUpgradeEligibilityResponseOutputValidationErrors0 struct { - Dependents []string `json:"dependents"` - Type ProjectUpgradeEligibilityResponseOutputValidationErrors0Type `json:"type"` -} - -// ProjectUpgradeEligibilityResponseOutputValidationErrors0Type defines model for ProjectUpgradeEligibilityResponseOutput.ValidationErrors.0.Type. -type ProjectUpgradeEligibilityResponseOutputValidationErrors0Type string - -// ProjectUpgradeEligibilityResponseOutputValidationErrors1 defines model for . -type ProjectUpgradeEligibilityResponseOutputValidationErrors1 struct { - IndexName string `json:"index_name"` - SchemaName string `json:"schema_name"` - TableName string `json:"table_name"` - Type ProjectUpgradeEligibilityResponseOutputValidationErrors1Type `json:"type"` -} - -// ProjectUpgradeEligibilityResponseOutputValidationErrors1Type defines model for ProjectUpgradeEligibilityResponseOutput.ValidationErrors.1.Type. -type ProjectUpgradeEligibilityResponseOutputValidationErrors1Type string - -// ProjectUpgradeEligibilityResponseOutputValidationErrors2 defines model for . -type ProjectUpgradeEligibilityResponseOutputValidationErrors2 struct { - FunctionName string `json:"function_name"` - LangName string `json:"lang_name"` - SchemaName string `json:"schema_name"` - Type ProjectUpgradeEligibilityResponseOutputValidationErrors2Type `json:"type"` -} - -// ProjectUpgradeEligibilityResponseOutputValidationErrors2Type defines model for ProjectUpgradeEligibilityResponseOutput.ValidationErrors.2.Type. -type ProjectUpgradeEligibilityResponseOutputValidationErrors2Type string - -// ProjectUpgradeEligibilityResponseOutputValidationErrors3 defines model for . -type ProjectUpgradeEligibilityResponseOutputValidationErrors3 struct { - ExtensionName string `json:"extension_name"` - Type ProjectUpgradeEligibilityResponseOutputValidationErrors3Type `json:"type"` -} - -// ProjectUpgradeEligibilityResponseOutputValidationErrors3Type defines model for ProjectUpgradeEligibilityResponseOutput.ValidationErrors.3.Type. -type ProjectUpgradeEligibilityResponseOutputValidationErrors3Type string - -// ProjectUpgradeEligibilityResponseOutputValidationErrors4 defines model for . -type ProjectUpgradeEligibilityResponseOutputValidationErrors4 struct { - FdwHandlerName string `json:"fdw_handler_name"` - FdwName string `json:"fdw_name"` - Type ProjectUpgradeEligibilityResponseOutputValidationErrors4Type `json:"type"` -} - -// ProjectUpgradeEligibilityResponseOutputValidationErrors4Type defines model for ProjectUpgradeEligibilityResponseOutput.ValidationErrors.4.Type. -type ProjectUpgradeEligibilityResponseOutputValidationErrors4Type string - -// ProjectUpgradeEligibilityResponseOutputValidationErrors5 defines model for . -type ProjectUpgradeEligibilityResponseOutputValidationErrors5 struct { - SchemaName string `json:"schema_name"` - SequenceName string `json:"sequence_name"` - TableName string `json:"table_name"` - Type ProjectUpgradeEligibilityResponseOutputValidationErrors5Type `json:"type"` -} - -// ProjectUpgradeEligibilityResponseOutputValidationErrors5Type defines model for ProjectUpgradeEligibilityResponseOutput.ValidationErrors.5.Type. -type ProjectUpgradeEligibilityResponseOutputValidationErrors5Type string - -// ProjectUpgradeEligibilityResponseOutputValidationErrors6 defines model for . -type ProjectUpgradeEligibilityResponseOutputValidationErrors6 struct { - ObjName string `json:"obj_name"` - ObjType ProjectUpgradeEligibilityResponseOutput_ValidationErrors_6_ObjType `json:"obj_type"` - SchemaName string `json:"schema_name"` - Type ProjectUpgradeEligibilityResponseOutputValidationErrors6Type `json:"type"` -} - -// ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType0 defines model for ProjectUpgradeEligibilityResponseOutput.ValidationErrors.6.ObjType.0. -type ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType0 string - -// ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType1 defines model for ProjectUpgradeEligibilityResponseOutput.ValidationErrors.6.ObjType.1. -type ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType1 string - -// ProjectUpgradeEligibilityResponseOutput_ValidationErrors_6_ObjType defines model for ProjectUpgradeEligibilityResponseOutput.ValidationErrors.6.ObjType. -type ProjectUpgradeEligibilityResponseOutput_ValidationErrors_6_ObjType struct { - union json.RawMessage -} - -// ProjectUpgradeEligibilityResponseOutputValidationErrors6Type defines model for ProjectUpgradeEligibilityResponseOutput.ValidationErrors.6.Type. -type ProjectUpgradeEligibilityResponseOutputValidationErrors6Type string - -// ProjectUpgradeEligibilityResponseOutputValidationErrors7 defines model for . -type ProjectUpgradeEligibilityResponseOutputValidationErrors7 struct { - SlotName string `json:"slot_name"` - Type ProjectUpgradeEligibilityResponseOutputValidationErrors7Type `json:"type"` -} - -// ProjectUpgradeEligibilityResponseOutputValidationErrors7Type defines model for ProjectUpgradeEligibilityResponseOutput.ValidationErrors.7.Type. -type ProjectUpgradeEligibilityResponseOutputValidationErrors7Type string - -// ProjectUpgradeEligibilityResponseOutputValidationErrors8 defines model for . -type ProjectUpgradeEligibilityResponseOutputValidationErrors8 struct { - Type ProjectUpgradeEligibilityResponseOutputValidationErrors8Type `json:"type"` -} - -// ProjectUpgradeEligibilityResponseOutputValidationErrors8Type defines model for ProjectUpgradeEligibilityResponseOutput.ValidationErrors.8.Type. -type ProjectUpgradeEligibilityResponseOutputValidationErrors8Type string - -// ProjectUpgradeEligibilityResponseOutputValidationErrors9 defines model for . -type ProjectUpgradeEligibilityResponseOutputValidationErrors9 struct { - Type ProjectUpgradeEligibilityResponseOutputValidationErrors9Type `json:"type"` -} - -// ProjectUpgradeEligibilityResponseOutputValidationErrors9Type defines model for ProjectUpgradeEligibilityResponseOutput.ValidationErrors.9.Type. -type ProjectUpgradeEligibilityResponseOutputValidationErrors9Type string - -// ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item defines model for ProjectUpgradeEligibilityResponse_Output.validation_errors.Item. -type ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item struct { - union json.RawMessage -} - -// ProjectUpgradeEligibilityResponseOutputWarnings0 defines model for . -type ProjectUpgradeEligibilityResponseOutputWarnings0 struct { - Type ProjectUpgradeEligibilityResponseOutputWarnings0Type `json:"type"` -} - -// ProjectUpgradeEligibilityResponseOutputWarnings0Type defines model for ProjectUpgradeEligibilityResponseOutput.Warnings.0.Type. -type ProjectUpgradeEligibilityResponseOutputWarnings0Type string - -// ProjectUpgradeEligibilityResponseOutputWarnings1 defines model for . -type ProjectUpgradeEligibilityResponseOutputWarnings1 struct { - Type ProjectUpgradeEligibilityResponseOutputWarnings1Type `json:"type"` -} - -// ProjectUpgradeEligibilityResponseOutputWarnings1Type defines model for ProjectUpgradeEligibilityResponseOutput.Warnings.1.Type. -type ProjectUpgradeEligibilityResponseOutputWarnings1Type string - -// ProjectUpgradeEligibilityResponseOutputWarnings2 defines model for . -type ProjectUpgradeEligibilityResponseOutputWarnings2 struct { - Type ProjectUpgradeEligibilityResponseOutputWarnings2Type `json:"type"` -} - -// ProjectUpgradeEligibilityResponseOutputWarnings2Type defines model for ProjectUpgradeEligibilityResponseOutput.Warnings.2.Type. -type ProjectUpgradeEligibilityResponseOutputWarnings2Type string - -// ProjectUpgradeEligibilityResponseOutputWarnings3 defines model for . -type ProjectUpgradeEligibilityResponseOutputWarnings3 struct { - Type ProjectUpgradeEligibilityResponseOutputWarnings3Type `json:"type"` -} - -// ProjectUpgradeEligibilityResponseOutputWarnings3Type defines model for ProjectUpgradeEligibilityResponseOutput.Warnings.3.Type. -type ProjectUpgradeEligibilityResponseOutputWarnings3Type string - -// ProjectUpgradeEligibilityResponseOutput_Warnings_Item defines model for ProjectUpgradeEligibilityResponse_Output.warnings.Item. -type ProjectUpgradeEligibilityResponseOutput_Warnings_Item struct { - union json.RawMessage -} - -// ProjectUpgradeInitiateResponseOutput defines model for ProjectUpgradeInitiateResponse_Output. -type ProjectUpgradeInitiateResponseOutput struct { - TrackingId string `json:"tracking_id"` -} - -// ReadOnlyStatusResponseOutput defines model for ReadOnlyStatusResponse_Output. -type ReadOnlyStatusResponseOutput struct { - Enabled bool `json:"enabled"` - OverrideActiveUntil string `json:"override_active_until"` - OverrideEnabled bool `json:"override_enabled"` -} - -// RealtimeConfigResponseOutput defines model for RealtimeConfigResponse_Output. -type RealtimeConfigResponseOutput struct { - // AdminSuspendedAt If set, the Realtime service has been suspended by an admin. - AdminSuspendedAt nullable.Nullable[time.Time] `json:"admin_suspended_at"` - - // ConnectionPool Sets connection pool size for Realtime Authorization - ConnectionPool nullable.Nullable[int] `json:"connection_pool"` - - // MaxBytesPerSecond Sets maximum number of bytes per second rate per channel limit - MaxBytesPerSecond nullable.Nullable[int] `json:"max_bytes_per_second"` - - // MaxChannelsPerClient Sets maximum number of channels per client rate limit - MaxChannelsPerClient nullable.Nullable[int] `json:"max_channels_per_client"` - - // MaxConcurrentUsers Sets maximum number of concurrent users rate limit - MaxConcurrentUsers nullable.Nullable[int] `json:"max_concurrent_users"` - - // MaxEventsPerSecond Sets maximum number of events per second rate per channel limit - MaxEventsPerSecond nullable.Nullable[int] `json:"max_events_per_second"` - - // MaxJoinsPerSecond Sets maximum number of joins per second rate limit - MaxJoinsPerSecond nullable.Nullable[int] `json:"max_joins_per_second"` - - // MaxPayloadSizeInKb Sets maximum number of payload size in KB rate limit - MaxPayloadSizeInKb nullable.Nullable[int] `json:"max_payload_size_in_kb"` - - // MaxPresenceEventsPerSecond Sets maximum number of presence events per second rate limit - MaxPresenceEventsPerSecond nullable.Nullable[int] `json:"max_presence_events_per_second"` - - // PostgresChangesPool Sets connection pool size used to create Postgres Changes subscriptions - PostgresChangesPool nullable.Nullable[int] `json:"postgres_changes_pool"` - - // PresenceEnabled Whether to enable presence - PresenceEnabled bool `json:"presence_enabled"` - - // PrivateOnly Whether to only allow private channels - PrivateOnly nullable.Nullable[bool] `json:"private_only"` - - // Suspend Disables the Realtime service for this project when true. Set to false to re-enable it. - Suspend nullable.Nullable[bool] `json:"suspend"` -} - -// RegionsInfoV1Output defines model for RegionsInfoV1_Output. -type RegionsInfoV1Output struct { - All struct { - SmartGroup []struct { - Code RegionsInfoV1OutputAllSmartGroupCode `json:"code"` - Name string `json:"name"` - Type RegionsInfoV1OutputAllSmartGroupType `json:"type"` - } `json:"smartGroup"` - Specific []struct { - Code RegionsInfoV1OutputAllSpecificCode `json:"code"` - Name string `json:"name"` - Provider RegionsInfoV1OutputAllSpecificProvider `json:"provider"` - Status *RegionsInfoV1OutputAllSpecificStatus `json:"status,omitempty"` - Type RegionsInfoV1OutputAllSpecificType `json:"type"` - } `json:"specific"` - } `json:"all"` - Recommendations struct { - SmartGroup struct { - Code RegionsInfoV1OutputRecommendationsSmartGroupCode `json:"code"` - Name string `json:"name"` - Type RegionsInfoV1OutputRecommendationsSmartGroupType `json:"type"` - } `json:"smartGroup"` - Specific []struct { - Code RegionsInfoV1OutputRecommendationsSpecificCode `json:"code"` - Name string `json:"name"` - Provider RegionsInfoV1OutputRecommendationsSpecificProvider `json:"provider"` - Status *RegionsInfoV1OutputRecommendationsSpecificStatus `json:"status,omitempty"` - Type RegionsInfoV1OutputRecommendationsSpecificType `json:"type"` - } `json:"specific"` - } `json:"recommendations"` -} - -// RegionsInfoV1OutputAllSmartGroupCode defines model for RegionsInfoV1Output.All.SmartGroup.Code. -type RegionsInfoV1OutputAllSmartGroupCode string - -// RegionsInfoV1OutputAllSmartGroupType defines model for RegionsInfoV1Output.All.SmartGroup.Type. -type RegionsInfoV1OutputAllSmartGroupType string - -// RegionsInfoV1OutputAllSpecificCode defines model for RegionsInfoV1Output.All.Specific.Code. -type RegionsInfoV1OutputAllSpecificCode string - -// RegionsInfoV1OutputAllSpecificProvider defines model for RegionsInfoV1Output.All.Specific.Provider. -type RegionsInfoV1OutputAllSpecificProvider string - -// RegionsInfoV1OutputAllSpecificStatus defines model for RegionsInfoV1Output.All.Specific.Status. -type RegionsInfoV1OutputAllSpecificStatus string - -// RegionsInfoV1OutputAllSpecificType defines model for RegionsInfoV1Output.All.Specific.Type. -type RegionsInfoV1OutputAllSpecificType string - -// RegionsInfoV1OutputRecommendationsSmartGroupCode defines model for RegionsInfoV1Output.Recommendations.SmartGroup.Code. -type RegionsInfoV1OutputRecommendationsSmartGroupCode string - -// RegionsInfoV1OutputRecommendationsSmartGroupType defines model for RegionsInfoV1Output.Recommendations.SmartGroup.Type. -type RegionsInfoV1OutputRecommendationsSmartGroupType string - -// RegionsInfoV1OutputRecommendationsSpecificCode defines model for RegionsInfoV1Output.Recommendations.Specific.Code. -type RegionsInfoV1OutputRecommendationsSpecificCode string - -// RegionsInfoV1OutputRecommendationsSpecificProvider defines model for RegionsInfoV1Output.Recommendations.Specific.Provider. -type RegionsInfoV1OutputRecommendationsSpecificProvider string - -// RegionsInfoV1OutputRecommendationsSpecificStatus defines model for RegionsInfoV1Output.Recommendations.Specific.Status. -type RegionsInfoV1OutputRecommendationsSpecificStatus string - -// RegionsInfoV1OutputRecommendationsSpecificType defines model for RegionsInfoV1Output.Recommendations.Specific.Type. -type RegionsInfoV1OutputRecommendationsSpecificType string - -// RemoveNetworkBanRequest defines model for RemoveNetworkBanRequest. -type RemoveNetworkBanRequest struct { - Identifier *string `json:"identifier,omitempty"` - - // Ipv4Addresses List of IP addresses to unban. - Ipv4Addresses []string `json:"ipv4_addresses"` - - // RequesterIp Include requester's public IP in the list of addresses to unban. - RequesterIp *bool `json:"requester_ip,omitempty"` -} - -// RemoveReadReplicaBody defines model for RemoveReadReplicaBody. -type RemoveReadReplicaBody struct { - DatabaseIdentifier string `json:"database_identifier"` -} - -// SecretResponseOutput defines model for SecretResponse_Output. -type SecretResponseOutput struct { - Name string `json:"name"` - UpdatedAt *string `json:"updated_at,omitempty"` - Value string `json:"value"` -} - -// SetUpReadReplicaBody defines model for SetUpReadReplicaBody. -type SetUpReadReplicaBody struct { - // ReadReplicaRegion Region you want your read replica to reside in - ReadReplicaRegion SetUpReadReplicaBodyReadReplicaRegion `json:"read_replica_region"` -} - -// SetUpReadReplicaBodyReadReplicaRegion Region you want your read replica to reside in -type SetUpReadReplicaBodyReadReplicaRegion string - -// SigningKeyResponseOutput defines model for SigningKeyResponse_Output. -type SigningKeyResponseOutput struct { - Algorithm SigningKeyResponseOutputAlgorithm `json:"algorithm"` - CreatedAt time.Time `json:"created_at"` - Id openapi_types.UUID `json:"id"` - PublicJwk nullable.Nullable[interface{}] `json:"public_jwk"` - Status SigningKeyResponseOutputStatus `json:"status"` - UpdatedAt time.Time `json:"updated_at"` -} - -// SigningKeyResponseOutputAlgorithm defines model for SigningKeyResponseOutput.Algorithm. -type SigningKeyResponseOutputAlgorithm string - -// SigningKeyResponseOutputStatus defines model for SigningKeyResponseOutput.Status. -type SigningKeyResponseOutputStatus string - -// SigningKeysResponseOutput defines model for SigningKeysResponse_Output. -type SigningKeysResponseOutput struct { - Keys []struct { - Algorithm SigningKeysResponseOutputKeysAlgorithm `json:"algorithm"` - CreatedAt time.Time `json:"created_at"` - Id openapi_types.UUID `json:"id"` - PublicJwk nullable.Nullable[interface{}] `json:"public_jwk"` - Status SigningKeysResponseOutputKeysStatus `json:"status"` - UpdatedAt time.Time `json:"updated_at"` - } `json:"keys"` -} - -// SigningKeysResponseOutputKeysAlgorithm defines model for SigningKeysResponseOutput.Keys.Algorithm. -type SigningKeysResponseOutputKeysAlgorithm string - -// SigningKeysResponseOutputKeysStatus defines model for SigningKeysResponseOutput.Keys.Status. -type SigningKeysResponseOutputKeysStatus string - -// SnippetListOutput defines model for SnippetList_Output. -type SnippetListOutput struct { - Cursor *string `json:"cursor,omitempty"` - Data []struct { - Description nullable.Nullable[string] `json:"description"` - Favorite bool `json:"favorite"` - Id string `json:"id"` - InsertedAt string `json:"inserted_at"` - Name string `json:"name"` - Owner struct { - Id float32 `json:"id"` - Username string `json:"username"` - } `json:"owner"` - Project struct { - Id float32 `json:"id"` - Name string `json:"name"` - } `json:"project"` - Type SnippetListOutputDataType `json:"type"` - UpdatedAt string `json:"updated_at"` - UpdatedBy struct { - Id float32 `json:"id"` - Username string `json:"username"` - } `json:"updated_by"` - Visibility SnippetListOutputDataVisibility `json:"visibility"` - } `json:"data"` -} - -// SnippetListOutputDataType defines model for SnippetListOutput.Data.Type. -type SnippetListOutputDataType string - -// SnippetListOutputDataVisibility defines model for SnippetListOutput.Data.Visibility. -type SnippetListOutputDataVisibility string - -// SnippetResponseOutput defines model for SnippetResponse_Output. -type SnippetResponseOutput struct { - Content struct { - // Favorite Deprecated: Rely on root-level favorite property instead. - // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set - Favorite *bool `json:"favorite,omitempty"` - SchemaVersion *string `json:"schema_version,omitempty"` - Sql string `json:"sql"` - } `json:"content"` - Description nullable.Nullable[string] `json:"description"` - Favorite bool `json:"favorite"` - Id string `json:"id"` - InsertedAt string `json:"inserted_at"` - Name string `json:"name"` - Owner struct { - Id float32 `json:"id"` - Username string `json:"username"` - } `json:"owner"` - Project struct { - Id float32 `json:"id"` - Name string `json:"name"` - } `json:"project"` - Type SnippetResponseOutputType `json:"type"` - UpdatedAt string `json:"updated_at"` - UpdatedBy struct { - Id float32 `json:"id"` - Username string `json:"username"` - } `json:"updated_by"` - Visibility SnippetResponseOutputVisibility `json:"visibility"` -} - -// SnippetResponseOutputType defines model for SnippetResponseOutput.Type. -type SnippetResponseOutputType string - -// SnippetResponseOutputVisibility defines model for SnippetResponseOutput.Visibility. -type SnippetResponseOutputVisibility string - -// SslEnforcementRequest defines model for SslEnforcementRequest. -type SslEnforcementRequest struct { - RequestedConfig struct { - Database bool `json:"database"` - } `json:"requestedConfig"` -} - -// SslEnforcementResponseOutput defines model for SslEnforcementResponse_Output. -type SslEnforcementResponseOutput struct { - AppliedSuccessfully bool `json:"appliedSuccessfully"` - CurrentConfig struct { - Database bool `json:"database"` - } `json:"currentConfig"` -} - -// StorageConfigResponseOutput defines model for StorageConfigResponse_Output. -type StorageConfigResponseOutput struct { - Capabilities struct { - IcebergCatalog bool `json:"iceberg_catalog"` - ListV2 bool `json:"list_v2"` - ObjectVersioning bool `json:"object_versioning"` - } `json:"capabilities"` - External struct { - UpstreamTarget StorageConfigResponseOutputExternalUpstreamTarget `json:"upstreamTarget"` - } `json:"external"` - Features struct { - IcebergCatalog struct { - Enabled bool `json:"enabled"` - MaxCatalogs int `json:"maxCatalogs"` - MaxNamespaces int `json:"maxNamespaces"` - MaxTables int `json:"maxTables"` - } `json:"icebergCatalog"` - ImageTransformation struct { - Enabled bool `json:"enabled"` - } `json:"imageTransformation"` - PurgeCache struct { - Enabled bool `json:"enabled"` - } `json:"purgeCache"` - S3Protocol struct { - Enabled bool `json:"enabled"` - } `json:"s3Protocol"` - VectorBuckets struct { - Enabled bool `json:"enabled"` - MaxBuckets int `json:"maxBuckets"` - MaxIndexes int `json:"maxIndexes"` - } `json:"vectorBuckets"` - } `json:"features"` - FileSizeLimit int64 `json:"fileSizeLimit"` - MigrationVersion nullable.Nullable[string] `json:"migrationVersion"` -} - -// StorageConfigResponseOutputExternalUpstreamTarget defines model for StorageConfigResponseOutput.External.UpstreamTarget. -type StorageConfigResponseOutputExternalUpstreamTarget string - -// StreamableFile defines model for StreamableFile. -type StreamableFile = map[string]interface{} - -// SubdomainAvailabilityResponseOutput defines model for SubdomainAvailabilityResponse_Output. -type SubdomainAvailabilityResponseOutput struct { - Available bool `json:"available"` -} - -// SupavisorConfigResponseOutput defines model for SupavisorConfigResponse_Output. -type SupavisorConfigResponseOutput struct { - ConnectionString string `json:"connection_string"` - DatabaseType SupavisorConfigResponseOutputDatabaseType `json:"database_type"` - DbHost string `json:"db_host"` - DbName string `json:"db_name"` - DbPort int `json:"db_port"` - DbUser string `json:"db_user"` - DefaultPoolSize nullable.Nullable[int] `json:"default_pool_size"` - Identifier string `json:"identifier"` - IsUsingScramAuth bool `json:"is_using_scram_auth"` - MaxClientConn nullable.Nullable[int] `json:"max_client_conn"` - PoolMode SupavisorConfigResponseOutputPoolMode `json:"pool_mode"` -} - -// SupavisorConfigResponseOutputDatabaseType defines model for SupavisorConfigResponseOutput.DatabaseType. -type SupavisorConfigResponseOutputDatabaseType string - -// SupavisorConfigResponseOutputPoolMode defines model for SupavisorConfigResponseOutput.PoolMode. -type SupavisorConfigResponseOutputPoolMode string - -// ThirdPartyAuthOutput defines model for ThirdPartyAuth_Output. -type ThirdPartyAuthOutput struct { - CustomJwks nullable.Nullable[interface{}] `json:"custom_jwks,omitempty"` - Id openapi_types.UUID `json:"id"` - InsertedAt string `json:"inserted_at"` - JwksUrl nullable.Nullable[string] `json:"jwks_url,omitempty"` - OidcIssuerUrl nullable.Nullable[string] `json:"oidc_issuer_url,omitempty"` - ResolvedAt nullable.Nullable[string] `json:"resolved_at,omitempty"` - ResolvedJwks nullable.Nullable[interface{}] `json:"resolved_jwks,omitempty"` - Type string `json:"type"` - UpdatedAt string `json:"updated_at"` -} - -// TypescriptResponseOutput defines model for TypescriptResponse_Output. -type TypescriptResponseOutput struct { - Types string `json:"types"` -} - -// UpdateApiKeyBody defines model for UpdateApiKeyBody. -type UpdateApiKeyBody struct { - Description nullable.Nullable[string] `json:"description,omitempty"` - Name *string `json:"name,omitempty"` - SecretJwtTemplate nullable.Nullable[map[string]interface{}] `json:"secret_jwt_template,omitempty"` -} - -// UpdateAuthConfigBody defines model for UpdateAuthConfigBody. -type UpdateAuthConfigBody struct { - ApiMaxRequestDuration nullable.Nullable[int] `json:"api_max_request_duration,omitempty"` - CustomOauthEnabled *bool `json:"custom_oauth_enabled,omitempty"` - DbMaxPoolSize nullable.Nullable[int] `json:"db_max_pool_size,omitempty"` - DbMaxPoolSizeUnit nullable.Nullable[UpdateAuthConfigBodyDbMaxPoolSizeUnit] `json:"db_max_pool_size_unit,omitempty"` - DisableSignup nullable.Nullable[bool] `json:"disable_signup,omitempty"` - ExternalAnonymousUsersEnabled nullable.Nullable[bool] `json:"external_anonymous_users_enabled,omitempty"` - ExternalAppleAdditionalClientIds nullable.Nullable[string] `json:"external_apple_additional_client_ids,omitempty"` - ExternalAppleClientId nullable.Nullable[string] `json:"external_apple_client_id,omitempty"` - ExternalAppleEmailOptional nullable.Nullable[bool] `json:"external_apple_email_optional,omitempty"` - ExternalAppleEnabled nullable.Nullable[bool] `json:"external_apple_enabled,omitempty"` - ExternalAppleSecret nullable.Nullable[string] `json:"external_apple_secret,omitempty"` - ExternalAzureClientId nullable.Nullable[string] `json:"external_azure_client_id,omitempty"` - ExternalAzureEmailOptional nullable.Nullable[bool] `json:"external_azure_email_optional,omitempty"` - ExternalAzureEnabled nullable.Nullable[bool] `json:"external_azure_enabled,omitempty"` - ExternalAzureSecret nullable.Nullable[string] `json:"external_azure_secret,omitempty"` - ExternalAzureUrl nullable.Nullable[string] `json:"external_azure_url,omitempty"` - ExternalBitbucketClientId nullable.Nullable[string] `json:"external_bitbucket_client_id,omitempty"` - ExternalBitbucketEmailOptional nullable.Nullable[bool] `json:"external_bitbucket_email_optional,omitempty"` - ExternalBitbucketEnabled nullable.Nullable[bool] `json:"external_bitbucket_enabled,omitempty"` - ExternalBitbucketSecret nullable.Nullable[string] `json:"external_bitbucket_secret,omitempty"` - ExternalDiscordClientId nullable.Nullable[string] `json:"external_discord_client_id,omitempty"` - ExternalDiscordEmailOptional nullable.Nullable[bool] `json:"external_discord_email_optional,omitempty"` - ExternalDiscordEnabled nullable.Nullable[bool] `json:"external_discord_enabled,omitempty"` - ExternalDiscordSecret nullable.Nullable[string] `json:"external_discord_secret,omitempty"` - ExternalEmailEnabled nullable.Nullable[bool] `json:"external_email_enabled,omitempty"` - ExternalFacebookClientId nullable.Nullable[string] `json:"external_facebook_client_id,omitempty"` - ExternalFacebookEmailOptional nullable.Nullable[bool] `json:"external_facebook_email_optional,omitempty"` - ExternalFacebookEnabled nullable.Nullable[bool] `json:"external_facebook_enabled,omitempty"` - ExternalFacebookSecret nullable.Nullable[string] `json:"external_facebook_secret,omitempty"` - ExternalFigmaClientId nullable.Nullable[string] `json:"external_figma_client_id,omitempty"` - ExternalFigmaEmailOptional nullable.Nullable[bool] `json:"external_figma_email_optional,omitempty"` - ExternalFigmaEnabled nullable.Nullable[bool] `json:"external_figma_enabled,omitempty"` - ExternalFigmaSecret nullable.Nullable[string] `json:"external_figma_secret,omitempty"` - ExternalGithubClientId nullable.Nullable[string] `json:"external_github_client_id,omitempty"` - ExternalGithubEmailOptional nullable.Nullable[bool] `json:"external_github_email_optional,omitempty"` - ExternalGithubEnabled nullable.Nullable[bool] `json:"external_github_enabled,omitempty"` - ExternalGithubSecret nullable.Nullable[string] `json:"external_github_secret,omitempty"` - ExternalGitlabClientId nullable.Nullable[string] `json:"external_gitlab_client_id,omitempty"` - ExternalGitlabEmailOptional nullable.Nullable[bool] `json:"external_gitlab_email_optional,omitempty"` - ExternalGitlabEnabled nullable.Nullable[bool] `json:"external_gitlab_enabled,omitempty"` - ExternalGitlabSecret nullable.Nullable[string] `json:"external_gitlab_secret,omitempty"` - ExternalGitlabUrl nullable.Nullable[string] `json:"external_gitlab_url,omitempty"` - ExternalGoogleAdditionalClientIds nullable.Nullable[string] `json:"external_google_additional_client_ids,omitempty"` - ExternalGoogleClientId nullable.Nullable[string] `json:"external_google_client_id,omitempty"` - ExternalGoogleEmailOptional nullable.Nullable[bool] `json:"external_google_email_optional,omitempty"` - ExternalGoogleEnabled nullable.Nullable[bool] `json:"external_google_enabled,omitempty"` - ExternalGoogleSecret nullable.Nullable[string] `json:"external_google_secret,omitempty"` - ExternalGoogleSkipNonceCheck nullable.Nullable[bool] `json:"external_google_skip_nonce_check,omitempty"` - ExternalKakaoClientId nullable.Nullable[string] `json:"external_kakao_client_id,omitempty"` - ExternalKakaoEmailOptional nullable.Nullable[bool] `json:"external_kakao_email_optional,omitempty"` - ExternalKakaoEnabled nullable.Nullable[bool] `json:"external_kakao_enabled,omitempty"` - ExternalKakaoSecret nullable.Nullable[string] `json:"external_kakao_secret,omitempty"` - ExternalKeycloakClientId nullable.Nullable[string] `json:"external_keycloak_client_id,omitempty"` - ExternalKeycloakEmailOptional nullable.Nullable[bool] `json:"external_keycloak_email_optional,omitempty"` - ExternalKeycloakEnabled nullable.Nullable[bool] `json:"external_keycloak_enabled,omitempty"` - ExternalKeycloakSecret nullable.Nullable[string] `json:"external_keycloak_secret,omitempty"` - ExternalKeycloakUrl nullable.Nullable[string] `json:"external_keycloak_url,omitempty"` - ExternalLinkedinOidcClientId nullable.Nullable[string] `json:"external_linkedin_oidc_client_id,omitempty"` - ExternalLinkedinOidcEmailOptional nullable.Nullable[bool] `json:"external_linkedin_oidc_email_optional,omitempty"` - ExternalLinkedinOidcEnabled nullable.Nullable[bool] `json:"external_linkedin_oidc_enabled,omitempty"` - ExternalLinkedinOidcSecret nullable.Nullable[string] `json:"external_linkedin_oidc_secret,omitempty"` - ExternalNotionClientId nullable.Nullable[string] `json:"external_notion_client_id,omitempty"` - ExternalNotionEmailOptional nullable.Nullable[bool] `json:"external_notion_email_optional,omitempty"` - ExternalNotionEnabled nullable.Nullable[bool] `json:"external_notion_enabled,omitempty"` - ExternalNotionSecret nullable.Nullable[string] `json:"external_notion_secret,omitempty"` - ExternalPhoneEnabled nullable.Nullable[bool] `json:"external_phone_enabled,omitempty"` - ExternalSlackClientId nullable.Nullable[string] `json:"external_slack_client_id,omitempty"` - ExternalSlackEmailOptional nullable.Nullable[bool] `json:"external_slack_email_optional,omitempty"` - ExternalSlackEnabled nullable.Nullable[bool] `json:"external_slack_enabled,omitempty"` - ExternalSlackOidcClientId nullable.Nullable[string] `json:"external_slack_oidc_client_id,omitempty"` - ExternalSlackOidcEmailOptional nullable.Nullable[bool] `json:"external_slack_oidc_email_optional,omitempty"` - ExternalSlackOidcEnabled nullable.Nullable[bool] `json:"external_slack_oidc_enabled,omitempty"` - ExternalSlackOidcSecret nullable.Nullable[string] `json:"external_slack_oidc_secret,omitempty"` - ExternalSlackSecret nullable.Nullable[string] `json:"external_slack_secret,omitempty"` - ExternalSpotifyClientId nullable.Nullable[string] `json:"external_spotify_client_id,omitempty"` - ExternalSpotifyEmailOptional nullable.Nullable[bool] `json:"external_spotify_email_optional,omitempty"` - ExternalSpotifyEnabled nullable.Nullable[bool] `json:"external_spotify_enabled,omitempty"` - ExternalSpotifySecret nullable.Nullable[string] `json:"external_spotify_secret,omitempty"` - ExternalTwitchClientId nullable.Nullable[string] `json:"external_twitch_client_id,omitempty"` - ExternalTwitchEmailOptional nullable.Nullable[bool] `json:"external_twitch_email_optional,omitempty"` - ExternalTwitchEnabled nullable.Nullable[bool] `json:"external_twitch_enabled,omitempty"` - ExternalTwitchSecret nullable.Nullable[string] `json:"external_twitch_secret,omitempty"` - ExternalTwitterClientId nullable.Nullable[string] `json:"external_twitter_client_id,omitempty"` - ExternalTwitterEmailOptional nullable.Nullable[bool] `json:"external_twitter_email_optional,omitempty"` - ExternalTwitterEnabled nullable.Nullable[bool] `json:"external_twitter_enabled,omitempty"` - ExternalTwitterSecret nullable.Nullable[string] `json:"external_twitter_secret,omitempty"` - ExternalWeb3EthereumEnabled nullable.Nullable[bool] `json:"external_web3_ethereum_enabled,omitempty"` - ExternalWeb3SolanaEnabled nullable.Nullable[bool] `json:"external_web3_solana_enabled,omitempty"` - ExternalWorkosClientId nullable.Nullable[string] `json:"external_workos_client_id,omitempty"` - ExternalWorkosEnabled nullable.Nullable[bool] `json:"external_workos_enabled,omitempty"` - ExternalWorkosSecret nullable.Nullable[string] `json:"external_workos_secret,omitempty"` - ExternalWorkosUrl nullable.Nullable[string] `json:"external_workos_url,omitempty"` - ExternalXClientId nullable.Nullable[string] `json:"external_x_client_id,omitempty"` - ExternalXEmailOptional nullable.Nullable[bool] `json:"external_x_email_optional,omitempty"` - ExternalXEnabled nullable.Nullable[bool] `json:"external_x_enabled,omitempty"` - ExternalXSecret nullable.Nullable[string] `json:"external_x_secret,omitempty"` - ExternalZoomClientId nullable.Nullable[string] `json:"external_zoom_client_id,omitempty"` - ExternalZoomEmailOptional nullable.Nullable[bool] `json:"external_zoom_email_optional,omitempty"` - ExternalZoomEnabled nullable.Nullable[bool] `json:"external_zoom_enabled,omitempty"` - ExternalZoomSecret nullable.Nullable[string] `json:"external_zoom_secret,omitempty"` - HookAfterUserCreatedEnabled nullable.Nullable[bool] `json:"hook_after_user_created_enabled,omitempty"` - HookAfterUserCreatedSecrets nullable.Nullable[string] `json:"hook_after_user_created_secrets,omitempty"` - HookAfterUserCreatedUri nullable.Nullable[string] `json:"hook_after_user_created_uri,omitempty"` - HookBeforeUserCreatedEnabled nullable.Nullable[bool] `json:"hook_before_user_created_enabled,omitempty"` - HookBeforeUserCreatedSecrets nullable.Nullable[string] `json:"hook_before_user_created_secrets,omitempty"` - HookBeforeUserCreatedUri nullable.Nullable[string] `json:"hook_before_user_created_uri,omitempty"` - HookCustomAccessTokenEnabled nullable.Nullable[bool] `json:"hook_custom_access_token_enabled,omitempty"` - HookCustomAccessTokenSecrets nullable.Nullable[string] `json:"hook_custom_access_token_secrets,omitempty"` - HookCustomAccessTokenUri nullable.Nullable[string] `json:"hook_custom_access_token_uri,omitempty"` - HookMfaVerificationAttemptEnabled nullable.Nullable[bool] `json:"hook_mfa_verification_attempt_enabled,omitempty"` - HookMfaVerificationAttemptSecrets nullable.Nullable[string] `json:"hook_mfa_verification_attempt_secrets,omitempty"` - HookMfaVerificationAttemptUri nullable.Nullable[string] `json:"hook_mfa_verification_attempt_uri,omitempty"` - HookPasswordVerificationAttemptEnabled nullable.Nullable[bool] `json:"hook_password_verification_attempt_enabled,omitempty"` - HookPasswordVerificationAttemptSecrets nullable.Nullable[string] `json:"hook_password_verification_attempt_secrets,omitempty"` - HookPasswordVerificationAttemptUri nullable.Nullable[string] `json:"hook_password_verification_attempt_uri,omitempty"` - HookSendEmailEnabled nullable.Nullable[bool] `json:"hook_send_email_enabled,omitempty"` - HookSendEmailSecrets nullable.Nullable[string] `json:"hook_send_email_secrets,omitempty"` - HookSendEmailUri nullable.Nullable[string] `json:"hook_send_email_uri,omitempty"` - HookSendSmsEnabled nullable.Nullable[bool] `json:"hook_send_sms_enabled,omitempty"` - HookSendSmsSecrets nullable.Nullable[string] `json:"hook_send_sms_secrets,omitempty"` - HookSendSmsUri nullable.Nullable[string] `json:"hook_send_sms_uri,omitempty"` - JwtExp nullable.Nullable[int] `json:"jwt_exp,omitempty"` - MailerAllowUnverifiedEmailSignIns nullable.Nullable[bool] `json:"mailer_allow_unverified_email_sign_ins,omitempty"` - MailerAutoconfirm nullable.Nullable[bool] `json:"mailer_autoconfirm,omitempty"` - MailerNotificationsEmailChangedEnabled nullable.Nullable[bool] `json:"mailer_notifications_email_changed_enabled,omitempty"` - MailerNotificationsIdentityLinkedEnabled nullable.Nullable[bool] `json:"mailer_notifications_identity_linked_enabled,omitempty"` - MailerNotificationsIdentityUnlinkedEnabled nullable.Nullable[bool] `json:"mailer_notifications_identity_unlinked_enabled,omitempty"` - MailerNotificationsMfaFactorEnrolledEnabled nullable.Nullable[bool] `json:"mailer_notifications_mfa_factor_enrolled_enabled,omitempty"` - MailerNotificationsMfaFactorUnenrolledEnabled nullable.Nullable[bool] `json:"mailer_notifications_mfa_factor_unenrolled_enabled,omitempty"` - MailerNotificationsPasswordChangedEnabled nullable.Nullable[bool] `json:"mailer_notifications_password_changed_enabled,omitempty"` - MailerNotificationsPhoneChangedEnabled nullable.Nullable[bool] `json:"mailer_notifications_phone_changed_enabled,omitempty"` - MailerOtpExp *int `json:"mailer_otp_exp,omitempty"` - MailerOtpLength nullable.Nullable[int] `json:"mailer_otp_length,omitempty"` - MailerSecureEmailChangeEnabled nullable.Nullable[bool] `json:"mailer_secure_email_change_enabled,omitempty"` - MailerSubjectsConfirmation nullable.Nullable[string] `json:"mailer_subjects_confirmation,omitempty"` - MailerSubjectsEmailChange nullable.Nullable[string] `json:"mailer_subjects_email_change,omitempty"` - MailerSubjectsEmailChangedNotification nullable.Nullable[string] `json:"mailer_subjects_email_changed_notification,omitempty"` - MailerSubjectsIdentityLinkedNotification nullable.Nullable[string] `json:"mailer_subjects_identity_linked_notification,omitempty"` - MailerSubjectsIdentityUnlinkedNotification nullable.Nullable[string] `json:"mailer_subjects_identity_unlinked_notification,omitempty"` - MailerSubjectsInvite nullable.Nullable[string] `json:"mailer_subjects_invite,omitempty"` - MailerSubjectsMagicLink nullable.Nullable[string] `json:"mailer_subjects_magic_link,omitempty"` - MailerSubjectsMfaFactorEnrolledNotification nullable.Nullable[string] `json:"mailer_subjects_mfa_factor_enrolled_notification,omitempty"` - MailerSubjectsMfaFactorUnenrolledNotification nullable.Nullable[string] `json:"mailer_subjects_mfa_factor_unenrolled_notification,omitempty"` - MailerSubjectsPasswordChangedNotification nullable.Nullable[string] `json:"mailer_subjects_password_changed_notification,omitempty"` - MailerSubjectsPhoneChangedNotification nullable.Nullable[string] `json:"mailer_subjects_phone_changed_notification,omitempty"` - MailerSubjectsReauthentication nullable.Nullable[string] `json:"mailer_subjects_reauthentication,omitempty"` - MailerSubjectsRecovery nullable.Nullable[string] `json:"mailer_subjects_recovery,omitempty"` - MailerTemplatesConfirmationContent nullable.Nullable[string] `json:"mailer_templates_confirmation_content,omitempty"` - MailerTemplatesEmailChangeContent nullable.Nullable[string] `json:"mailer_templates_email_change_content,omitempty"` - MailerTemplatesEmailChangedNotificationContent nullable.Nullable[string] `json:"mailer_templates_email_changed_notification_content,omitempty"` - MailerTemplatesIdentityLinkedNotificationContent nullable.Nullable[string] `json:"mailer_templates_identity_linked_notification_content,omitempty"` - MailerTemplatesIdentityUnlinkedNotificationContent nullable.Nullable[string] `json:"mailer_templates_identity_unlinked_notification_content,omitempty"` - MailerTemplatesInviteContent nullable.Nullable[string] `json:"mailer_templates_invite_content,omitempty"` - MailerTemplatesMagicLinkContent nullable.Nullable[string] `json:"mailer_templates_magic_link_content,omitempty"` - MailerTemplatesMfaFactorEnrolledNotificationContent nullable.Nullable[string] `json:"mailer_templates_mfa_factor_enrolled_notification_content,omitempty"` - MailerTemplatesMfaFactorUnenrolledNotificationContent nullable.Nullable[string] `json:"mailer_templates_mfa_factor_unenrolled_notification_content,omitempty"` - MailerTemplatesPasswordChangedNotificationContent nullable.Nullable[string] `json:"mailer_templates_password_changed_notification_content,omitempty"` - MailerTemplatesPhoneChangedNotificationContent nullable.Nullable[string] `json:"mailer_templates_phone_changed_notification_content,omitempty"` - MailerTemplatesReauthenticationContent nullable.Nullable[string] `json:"mailer_templates_reauthentication_content,omitempty"` - MailerTemplatesRecoveryContent nullable.Nullable[string] `json:"mailer_templates_recovery_content,omitempty"` - MfaMaxEnrolledFactors nullable.Nullable[int] `json:"mfa_max_enrolled_factors,omitempty"` - MfaPhoneEnrollEnabled nullable.Nullable[bool] `json:"mfa_phone_enroll_enabled,omitempty"` - MfaPhoneMaxFrequency nullable.Nullable[int] `json:"mfa_phone_max_frequency,omitempty"` - MfaPhoneOtpLength nullable.Nullable[int] `json:"mfa_phone_otp_length,omitempty"` - MfaPhoneTemplate nullable.Nullable[string] `json:"mfa_phone_template,omitempty"` - MfaPhoneVerifyEnabled nullable.Nullable[bool] `json:"mfa_phone_verify_enabled,omitempty"` - MfaTotpEnrollEnabled nullable.Nullable[bool] `json:"mfa_totp_enroll_enabled,omitempty"` - MfaTotpVerifyEnabled nullable.Nullable[bool] `json:"mfa_totp_verify_enabled,omitempty"` - MfaWebAuthnEnrollEnabled nullable.Nullable[bool] `json:"mfa_web_authn_enroll_enabled,omitempty"` - MfaWebAuthnVerifyEnabled nullable.Nullable[bool] `json:"mfa_web_authn_verify_enabled,omitempty"` - NimbusOauthClientId nullable.Nullable[string] `json:"nimbus_oauth_client_id,omitempty"` - NimbusOauthClientSecret nullable.Nullable[string] `json:"nimbus_oauth_client_secret,omitempty"` - OauthServerAllowDynamicRegistration nullable.Nullable[bool] `json:"oauth_server_allow_dynamic_registration,omitempty"` - OauthServerAuthorizationPath nullable.Nullable[string] `json:"oauth_server_authorization_path,omitempty"` - OauthServerEnabled nullable.Nullable[bool] `json:"oauth_server_enabled,omitempty"` - PasskeyEnabled *bool `json:"passkey_enabled,omitempty"` - PasswordHibpEnabled nullable.Nullable[bool] `json:"password_hibp_enabled,omitempty"` - PasswordMinLength nullable.Nullable[int] `json:"password_min_length,omitempty"` - PasswordRequiredCharacters nullable.Nullable[UpdateAuthConfigBodyPasswordRequiredCharacters] `json:"password_required_characters,omitempty"` - RateLimitAnonymousUsers nullable.Nullable[int] `json:"rate_limit_anonymous_users,omitempty"` - RateLimitEmailSent nullable.Nullable[int] `json:"rate_limit_email_sent,omitempty"` - RateLimitOtp nullable.Nullable[int] `json:"rate_limit_otp,omitempty"` - RateLimitSmsSent nullable.Nullable[int] `json:"rate_limit_sms_sent,omitempty"` - RateLimitTokenRefresh nullable.Nullable[int] `json:"rate_limit_token_refresh,omitempty"` - RateLimitVerify nullable.Nullable[int] `json:"rate_limit_verify,omitempty"` - RateLimitWeb3 nullable.Nullable[int] `json:"rate_limit_web3,omitempty"` - RefreshTokenRotationEnabled nullable.Nullable[bool] `json:"refresh_token_rotation_enabled,omitempty"` - SamlEnabled nullable.Nullable[bool] `json:"saml_enabled,omitempty"` - SamlExternalUrl nullable.Nullable[string] `json:"saml_external_url,omitempty"` - SecurityCaptchaEnabled nullable.Nullable[bool] `json:"security_captcha_enabled,omitempty"` - SecurityCaptchaProvider nullable.Nullable[UpdateAuthConfigBodySecurityCaptchaProvider] `json:"security_captcha_provider,omitempty"` - SecurityCaptchaSecret nullable.Nullable[string] `json:"security_captcha_secret,omitempty"` - SecurityManualLinkingEnabled nullable.Nullable[bool] `json:"security_manual_linking_enabled,omitempty"` - - // SecurityRefreshTokenReuseInterval Refresh token reuse interval in seconds. Maximum 300 seconds (5 minutes). - SecurityRefreshTokenReuseInterval nullable.Nullable[int] `json:"security_refresh_token_reuse_interval,omitempty"` - SecuritySbForwardedForEnabled nullable.Nullable[bool] `json:"security_sb_forwarded_for_enabled,omitempty"` - - // SecurityUpdatePasswordRequireCurrentPassword Require the user's current password when updating their password. - SecurityUpdatePasswordRequireCurrentPassword nullable.Nullable[bool] `json:"security_update_password_require_current_password,omitempty"` - SecurityUpdatePasswordRequireReauthentication nullable.Nullable[bool] `json:"security_update_password_require_reauthentication,omitempty"` - - // SessionsInactivityTimeout Session inactivity timeout in hours. Maximum 8760 hours (1 year). - SessionsInactivityTimeout nullable.Nullable[float32] `json:"sessions_inactivity_timeout,omitempty"` - SessionsSinglePerUser nullable.Nullable[bool] `json:"sessions_single_per_user,omitempty"` - SessionsTags nullable.Nullable[string] `json:"sessions_tags,omitempty"` - - // SessionsTimebox Session timebox in hours. Maximum 8760 hours (1 year). - SessionsTimebox nullable.Nullable[float32] `json:"sessions_timebox,omitempty"` - SiteUrl nullable.Nullable[string] `json:"site_url,omitempty"` - SmsAutoconfirm nullable.Nullable[bool] `json:"sms_autoconfirm,omitempty"` - SmsMaxFrequency nullable.Nullable[int] `json:"sms_max_frequency,omitempty"` - SmsMessagebirdAccessKey nullable.Nullable[string] `json:"sms_messagebird_access_key,omitempty"` - SmsMessagebirdOriginator nullable.Nullable[string] `json:"sms_messagebird_originator,omitempty"` - SmsOtpExp nullable.Nullable[int] `json:"sms_otp_exp,omitempty"` - SmsOtpLength *int `json:"sms_otp_length,omitempty"` - SmsProvider nullable.Nullable[UpdateAuthConfigBodySmsProvider] `json:"sms_provider,omitempty"` - SmsTemplate nullable.Nullable[string] `json:"sms_template,omitempty"` - SmsTestOtp nullable.Nullable[string] `json:"sms_test_otp,omitempty"` - SmsTestOtpValidUntil nullable.Nullable[time.Time] `json:"sms_test_otp_valid_until,omitempty"` - SmsTextlocalApiKey nullable.Nullable[string] `json:"sms_textlocal_api_key,omitempty"` - SmsTextlocalSender nullable.Nullable[string] `json:"sms_textlocal_sender,omitempty"` - SmsTwilioAccountSid nullable.Nullable[string] `json:"sms_twilio_account_sid,omitempty"` - SmsTwilioAuthToken nullable.Nullable[string] `json:"sms_twilio_auth_token,omitempty"` - SmsTwilioContentSid nullable.Nullable[string] `json:"sms_twilio_content_sid,omitempty"` - SmsTwilioMessageServiceSid nullable.Nullable[string] `json:"sms_twilio_message_service_sid,omitempty"` - SmsTwilioVerifyAccountSid nullable.Nullable[string] `json:"sms_twilio_verify_account_sid,omitempty"` - SmsTwilioVerifyAuthToken nullable.Nullable[string] `json:"sms_twilio_verify_auth_token,omitempty"` - SmsTwilioVerifyMessageServiceSid nullable.Nullable[string] `json:"sms_twilio_verify_message_service_sid,omitempty"` - SmsVonageApiKey nullable.Nullable[string] `json:"sms_vonage_api_key,omitempty"` - SmsVonageApiSecret nullable.Nullable[string] `json:"sms_vonage_api_secret,omitempty"` - SmsVonageFrom nullable.Nullable[string] `json:"sms_vonage_from,omitempty"` - SmtpAdminEmail nullable.Nullable[openapi_types.Email] `json:"smtp_admin_email,omitempty"` - SmtpHost nullable.Nullable[string] `json:"smtp_host,omitempty"` - SmtpMaxFrequency nullable.Nullable[int] `json:"smtp_max_frequency,omitempty"` - SmtpPass nullable.Nullable[string] `json:"smtp_pass,omitempty"` - SmtpPort nullable.Nullable[string] `json:"smtp_port,omitempty"` - SmtpSenderName nullable.Nullable[string] `json:"smtp_sender_name,omitempty"` - SmtpUser nullable.Nullable[string] `json:"smtp_user,omitempty"` - UriAllowList nullable.Nullable[string] `json:"uri_allow_list,omitempty"` - WebauthnRpDisplayName nullable.Nullable[string] `json:"webauthn_rp_display_name,omitempty"` - WebauthnRpId nullable.Nullable[string] `json:"webauthn_rp_id,omitempty"` - WebauthnRpOrigins nullable.Nullable[string] `json:"webauthn_rp_origins,omitempty"` -} - -// UpdateAuthConfigBodyDbMaxPoolSizeUnit defines model for UpdateAuthConfigBody.DbMaxPoolSizeUnit. -type UpdateAuthConfigBodyDbMaxPoolSizeUnit string - -// UpdateAuthConfigBodyPasswordRequiredCharacters defines model for UpdateAuthConfigBody.PasswordRequiredCharacters. -type UpdateAuthConfigBodyPasswordRequiredCharacters string - -// UpdateAuthConfigBodySecurityCaptchaProvider defines model for UpdateAuthConfigBody.SecurityCaptchaProvider. -type UpdateAuthConfigBodySecurityCaptchaProvider string - -// UpdateAuthConfigBodySmsProvider defines model for UpdateAuthConfigBody.SmsProvider. -type UpdateAuthConfigBodySmsProvider string - -// UpdateBranchBody defines model for UpdateBranchBody. -type UpdateBranchBody struct { - BranchName *string `json:"branch_name,omitempty"` - GitBranch *string `json:"git_branch,omitempty"` - - // NotifyUrl HTTP endpoint to receive branch status updates. - NotifyUrl *string `json:"notify_url,omitempty"` - Persistent *bool `json:"persistent,omitempty"` - RequestReview *bool `json:"request_review,omitempty"` - - // ResetOnPush This field is deprecated and will be ignored. Use v1-reset-a-branch endpoint directly instead. - // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set - ResetOnPush *bool `json:"reset_on_push,omitempty"` - Status *UpdateBranchBodyStatus `json:"status,omitempty"` -} - -// UpdateBranchBodyStatus defines model for UpdateBranchBody.Status. -type UpdateBranchBodyStatus string - -// UpdateCustomHostnameBody defines model for UpdateCustomHostnameBody. -type UpdateCustomHostnameBody struct { - CustomHostname string `json:"custom_hostname"` -} - -// UpdateCustomHostnameResponseOutput defines model for UpdateCustomHostnameResponse_Output. -type UpdateCustomHostnameResponseOutput struct { - CustomHostname *string `json:"custom_hostname,omitempty"` - Data struct { - Errors []JsonValueOutput `json:"errors"` - Messages []JsonValueOutput `json:"messages"` - Result struct { - CustomOriginServer *string `json:"custom_origin_server,omitempty"` - Hostname string `json:"hostname"` - Id string `json:"id"` - OwnershipVerification *struct { - Name *string `json:"name,omitempty"` - Type *string `json:"type,omitempty"` - Value *string `json:"value,omitempty"` - } `json:"ownership_verification,omitempty"` - Ssl *struct { - Status *string `json:"status,omitempty"` - ValidationErrors *[]struct { - Message string `json:"message"` - } `json:"validation_errors,omitempty"` - ValidationRecords *[]struct { - TxtName *string `json:"txt_name,omitempty"` - TxtValue *string `json:"txt_value,omitempty"` - } `json:"validation_records,omitempty"` - } `json:"ssl,omitempty"` - Status *string `json:"status,omitempty"` - VerificationErrors *[]string `json:"verification_errors,omitempty"` - } `json:"result"` - Success bool `json:"success"` - } `json:"data"` - Status UpdateCustomHostnameResponseOutputStatus `json:"status"` -} - -// UpdateCustomHostnameResponseOutputStatus defines model for UpdateCustomHostnameResponseOutput.Status. -type UpdateCustomHostnameResponseOutputStatus string - -// UpdateJitAccessBody defines model for UpdateJitAccessBody. -type UpdateJitAccessBody struct { - Roles []struct { - AllowedNetworks *struct { - AllowedCidrs *[]struct { - Cidr string `json:"cidr"` - } `json:"allowed_cidrs,omitempty"` - AllowedCidrsV6 *[]struct { - Cidr string `json:"cidr"` - } `json:"allowed_cidrs_v6,omitempty"` - } `json:"allowed_networks,omitempty"` - BranchesOnly *bool `json:"branches_only,omitempty"` - ExpiresAt *float32 `json:"expires_at,omitempty"` - Role string `json:"role"` - } `json:"roles"` - UserId openapi_types.UUID `json:"user_id"` -} - -// UpdatePgsodiumConfigBody defines model for UpdatePgsodiumConfigBody. -type UpdatePgsodiumConfigBody struct { - // RootKey The pgsodium root key: 32 bytes, hex-encoded (64 characters). - RootKey string `json:"root_key"` -} - -// UpdatePostgresConfigBody defines model for UpdatePostgresConfigBody. -type UpdatePostgresConfigBody struct { - AutovacuumMaxWorkers *int `json:"autovacuum_max_workers,omitempty"` - AutovacuumWorkMem *string `json:"autovacuum_work_mem,omitempty"` - - // CheckpointTimeout Default unit: s - CheckpointTimeout *string `json:"checkpoint_timeout,omitempty"` - CronLogStatement *bool `json:"cron.log_statement,omitempty"` - EffectiveCacheSize *string `json:"effective_cache_size,omitempty"` - HotStandbyFeedback *bool `json:"hot_standby_feedback,omitempty"` - - // LogAutovacuumMinDuration Default unit: ms - LogAutovacuumMinDuration *string `json:"log_autovacuum_min_duration,omitempty"` - LogCheckpoints *bool `json:"log_checkpoints,omitempty"` - LogConnections *bool `json:"log_connections,omitempty"` - LogDisconnections *bool `json:"log_disconnections,omitempty"` - LogDuration *bool `json:"log_duration,omitempty"` - LogLockWaits *bool `json:"log_lock_waits,omitempty"` - LogRecoveryConflictWaits *bool `json:"log_recovery_conflict_waits,omitempty"` - LogReplicationCommands *bool `json:"log_replication_commands,omitempty"` - - // LogStartupProgressInterval Default unit: ms - LogStartupProgressInterval *string `json:"log_startup_progress_interval,omitempty"` - LogTempFiles *string `json:"log_temp_files,omitempty"` - LogicalDecodingWorkMem *string `json:"logical_decoding_work_mem,omitempty"` - MaintenanceWorkMem *string `json:"maintenance_work_mem,omitempty"` - MaxConnections *int `json:"max_connections,omitempty"` - MaxLocksPerTransaction *int `json:"max_locks_per_transaction,omitempty"` - MaxLogicalReplicationWorkers *int `json:"max_logical_replication_workers,omitempty"` - MaxParallelMaintenanceWorkers *int `json:"max_parallel_maintenance_workers,omitempty"` - MaxParallelWorkers *int `json:"max_parallel_workers,omitempty"` - MaxParallelWorkersPerGather *int `json:"max_parallel_workers_per_gather,omitempty"` - MaxReplicationSlots *int `json:"max_replication_slots,omitempty"` - MaxSlotWalKeepSize *string `json:"max_slot_wal_keep_size,omitempty"` - MaxStandbyArchiveDelay *string `json:"max_standby_archive_delay,omitempty"` - MaxStandbyStreamingDelay *string `json:"max_standby_streaming_delay,omitempty"` - MaxSyncWorkersPerSubscription *int `json:"max_sync_workers_per_subscription,omitempty"` - MaxWalSenders *int `json:"max_wal_senders,omitempty"` - MaxWalSize *string `json:"max_wal_size,omitempty"` - MaxWorkerProcesses *int `json:"max_worker_processes,omitempty"` - PgNetBatchSize *int `json:"pg_net.batch_size,omitempty"` - PgStatStatementsMax *int `json:"pg_stat_statements.max,omitempty"` - RestartDatabase *bool `json:"restart_database,omitempty"` - SessionReplicationRole *UpdatePostgresConfigBodySessionReplicationRole `json:"session_replication_role,omitempty"` - SharedBuffers *string `json:"shared_buffers,omitempty"` - - // StatementTimeout Default unit: ms - StatementTimeout *string `json:"statement_timeout,omitempty"` - TrackActivityQuerySize *string `json:"track_activity_query_size,omitempty"` - TrackCommitTimestamp *bool `json:"track_commit_timestamp,omitempty"` - WalKeepSize *string `json:"wal_keep_size,omitempty"` - - // WalSenderTimeout Default unit: ms - WalSenderTimeout *string `json:"wal_sender_timeout,omitempty"` - WorkMem *string `json:"work_mem,omitempty"` -} - -// UpdatePostgresConfigBodySessionReplicationRole defines model for UpdatePostgresConfigBody.SessionReplicationRole. -type UpdatePostgresConfigBodySessionReplicationRole string - -// UpdateProviderBody defines model for UpdateProviderBody. -type UpdateProviderBody struct { - AttributeMapping *struct { - Keys map[string]struct { - Array *bool `json:"array,omitempty"` - Default interface{} `json:"default,omitempty"` - Name *string `json:"name,omitempty"` - Names *[]string `json:"names,omitempty"` - } `json:"keys"` - } `json:"attribute_mapping,omitempty"` - Domains *[]string `json:"domains,omitempty"` - MetadataUrl *string `json:"metadata_url,omitempty"` - MetadataXml *string `json:"metadata_xml,omitempty"` - NameIdFormat *UpdateProviderBodyNameIdFormat `json:"name_id_format,omitempty"` -} - -// UpdateProviderBodyNameIdFormat defines model for UpdateProviderBody.NameIdFormat. -type UpdateProviderBodyNameIdFormat string - -// UpdateProviderResponseOutput defines model for UpdateProviderResponse_Output. -type UpdateProviderResponseOutput struct { - CreatedAt *string `json:"created_at,omitempty"` - Domains *[]struct { - CreatedAt *string `json:"created_at,omitempty"` - Domain *string `json:"domain,omitempty"` - UpdatedAt *string `json:"updated_at,omitempty"` - } `json:"domains,omitempty"` - Id string `json:"id"` - Saml *struct { - AttributeMapping *struct { - Keys map[string]struct { - Array *bool `json:"array,omitempty"` - Default interface{} `json:"default,omitempty"` - Name *string `json:"name,omitempty"` - Names *[]string `json:"names,omitempty"` - } `json:"keys"` - } `json:"attribute_mapping,omitempty"` - EntityId string `json:"entity_id"` - MetadataUrl *string `json:"metadata_url,omitempty"` - MetadataXml *string `json:"metadata_xml,omitempty"` - NameIdFormat *string `json:"name_id_format,omitempty"` - } `json:"saml,omitempty"` - UpdatedAt *string `json:"updated_at,omitempty"` -} - -// UpdateRealtimeConfigBody defines model for UpdateRealtimeConfigBody. -type UpdateRealtimeConfigBody struct { - // ConnectionPool Sets connection pool size for Realtime Authorization - ConnectionPool *int `json:"connection_pool,omitempty"` - - // MaxBytesPerSecond Sets maximum number of bytes per second rate per channel limit - MaxBytesPerSecond *int `json:"max_bytes_per_second,omitempty"` - - // MaxChannelsPerClient Sets maximum number of channels per client rate limit - MaxChannelsPerClient *int `json:"max_channels_per_client,omitempty"` - - // MaxConcurrentUsers Sets maximum number of concurrent users rate limit - MaxConcurrentUsers *int `json:"max_concurrent_users,omitempty"` - - // MaxEventsPerSecond Sets maximum number of events per second rate per channel limit - MaxEventsPerSecond *int `json:"max_events_per_second,omitempty"` - - // MaxJoinsPerSecond Sets maximum number of joins per second rate limit - MaxJoinsPerSecond *int `json:"max_joins_per_second,omitempty"` - - // MaxPayloadSizeInKb Sets maximum number of payload size in KB rate limit - MaxPayloadSizeInKb *int `json:"max_payload_size_in_kb,omitempty"` - - // MaxPresenceEventsPerSecond Sets maximum number of presence events per second rate limit - MaxPresenceEventsPerSecond *int `json:"max_presence_events_per_second,omitempty"` - - // PostgresChangesPool Sets connection pool size used to create Postgres Changes subscriptions - PostgresChangesPool *int `json:"postgres_changes_pool,omitempty"` - - // PresenceEnabled Whether to enable presence - PresenceEnabled *bool `json:"presence_enabled,omitempty"` - - // PrivateOnly Whether to only allow private channels - PrivateOnly *bool `json:"private_only,omitempty"` - - // Suspend Disables the Realtime service for this project when true. Set to false to re-enable it. - Suspend *bool `json:"suspend,omitempty"` -} - -// UpdateRunStatusBody defines model for UpdateRunStatusBody. -type UpdateRunStatusBody struct { - Clone *UpdateRunStatusBodyClone `json:"clone,omitempty"` - Configure *UpdateRunStatusBodyConfigure `json:"configure,omitempty"` - Deploy *UpdateRunStatusBodyDeploy `json:"deploy,omitempty"` - Health *UpdateRunStatusBodyHealth `json:"health,omitempty"` - Migrate *UpdateRunStatusBodyMigrate `json:"migrate,omitempty"` - Pull *UpdateRunStatusBodyPull `json:"pull,omitempty"` - Seed *UpdateRunStatusBodySeed `json:"seed,omitempty"` -} - -// UpdateRunStatusBodyClone defines model for UpdateRunStatusBody.Clone. -type UpdateRunStatusBodyClone string - -// UpdateRunStatusBodyConfigure defines model for UpdateRunStatusBody.Configure. -type UpdateRunStatusBodyConfigure string - -// UpdateRunStatusBodyDeploy defines model for UpdateRunStatusBody.Deploy. -type UpdateRunStatusBodyDeploy string - -// UpdateRunStatusBodyHealth defines model for UpdateRunStatusBody.Health. -type UpdateRunStatusBodyHealth string - -// UpdateRunStatusBodyMigrate defines model for UpdateRunStatusBody.Migrate. -type UpdateRunStatusBodyMigrate string - -// UpdateRunStatusBodyPull defines model for UpdateRunStatusBody.Pull. -type UpdateRunStatusBodyPull string - -// UpdateRunStatusBodySeed defines model for UpdateRunStatusBody.Seed. -type UpdateRunStatusBodySeed string - -// UpdateRunStatusResponseOutput defines model for UpdateRunStatusResponse_Output. -type UpdateRunStatusResponseOutput struct { - Message UpdateRunStatusResponseOutputMessage `json:"message"` -} - -// UpdateRunStatusResponseOutputMessage defines model for UpdateRunStatusResponseOutput.Message. -type UpdateRunStatusResponseOutputMessage string - -// UpdateSigningKeyBody defines model for UpdateSigningKeyBody. -type UpdateSigningKeyBody struct { - Status UpdateSigningKeyBodyStatus `json:"status"` -} - -// UpdateSigningKeyBodyStatus defines model for UpdateSigningKeyBody.Status. -type UpdateSigningKeyBodyStatus string - -// UpdateStorageConfigBody defines model for UpdateStorageConfigBody. -type UpdateStorageConfigBody struct { - External *struct { - UpstreamTarget UpdateStorageConfigBodyExternalUpstreamTarget `json:"upstreamTarget"` - } `json:"external,omitempty"` - Features *struct { - IcebergCatalog *struct { - Enabled bool `json:"enabled"` - MaxCatalogs int `json:"maxCatalogs"` - MaxNamespaces int `json:"maxNamespaces"` - MaxTables int `json:"maxTables"` - } `json:"icebergCatalog,omitempty"` - ImageTransformation *struct { - Enabled bool `json:"enabled"` - } `json:"imageTransformation,omitempty"` - PurgeCache *struct { - Enabled bool `json:"enabled"` - } `json:"purgeCache,omitempty"` - S3Protocol *struct { - Enabled bool `json:"enabled"` - } `json:"s3Protocol,omitempty"` - VectorBuckets *struct { - Enabled bool `json:"enabled"` - MaxBuckets int `json:"maxBuckets"` - MaxIndexes int `json:"maxIndexes"` - } `json:"vectorBuckets,omitempty"` - } `json:"features,omitempty"` - FileSizeLimit *int64 `json:"fileSizeLimit,omitempty"` -} - -// UpdateStorageConfigBodyExternalUpstreamTarget defines model for UpdateStorageConfigBody.External.UpstreamTarget. -type UpdateStorageConfigBodyExternalUpstreamTarget string - -// UpdateSupavisorConfigBody defines model for UpdateSupavisorConfigBody. -type UpdateSupavisorConfigBody struct { - DefaultPoolSize nullable.Nullable[int] `json:"default_pool_size,omitempty"` - - // PoolMode Dedicated pooler mode for the project - PoolMode *UpdateSupavisorConfigBodyPoolMode `json:"pool_mode,omitempty"` -} - -// UpdateSupavisorConfigBodyPoolMode Dedicated pooler mode for the project -type UpdateSupavisorConfigBodyPoolMode string - -// UpdateSupavisorConfigResponseOutput defines model for UpdateSupavisorConfigResponse_Output. -type UpdateSupavisorConfigResponseOutput struct { - DefaultPoolSize nullable.Nullable[int] `json:"default_pool_size"` - PoolMode string `json:"pool_mode"` -} - -// UpgradeDatabaseBody defines model for UpgradeDatabaseBody. -type UpgradeDatabaseBody struct { - ReleaseChannel *UpgradeDatabaseBodyReleaseChannel `json:"release_channel,omitempty"` - TargetVersion string `json:"target_version"` -} - -// UpgradeDatabaseBodyReleaseChannel defines model for UpgradeDatabaseBody.ReleaseChannel. -type UpgradeDatabaseBodyReleaseChannel string - -// V1BackupScheduleResponseOutput defines model for V1BackupScheduleResponse_Output. -type V1BackupScheduleResponseOutput struct { - // ScheduleFor Time of day to schedule daily backups, in UTC. Format: HH:MM:SS. - ScheduleFor string `json:"schedule_for"` - - // UpdatedAt Timestamp of when the backup schedule was last updated. - UpdatedAt time.Time `json:"updated_at"` -} - -// V1BackupsResponseOutput defines model for V1BackupsResponse_Output. -type V1BackupsResponseOutput struct { - Backups []struct { - Id int `json:"id"` - InsertedAt string `json:"inserted_at"` - IsPhysicalBackup bool `json:"is_physical_backup"` - Status V1BackupsResponseOutputBackupsStatus `json:"status"` - } `json:"backups"` - PhysicalBackupData struct { - EarliestPhysicalBackupDateUnix *int `json:"earliest_physical_backup_date_unix,omitempty"` - LatestPhysicalBackupDateUnix *int `json:"latest_physical_backup_date_unix,omitempty"` - } `json:"physical_backup_data"` - PitrEnabled bool `json:"pitr_enabled"` - Region string `json:"region"` - WalgEnabled bool `json:"walg_enabled"` -} - -// V1BackupsResponseOutputBackupsStatus defines model for V1BackupsResponseOutput.Backups.Status. -type V1BackupsResponseOutputBackupsStatus string - -// V1CreateFunctionBody defines model for V1CreateFunctionBody. -type V1CreateFunctionBody struct { - Body string `json:"body"` - Name string `json:"name"` - Slug string `json:"slug"` - VerifyJwt *bool `json:"verify_jwt,omitempty"` -} - -// V1CreateMigrationBody defines model for V1CreateMigrationBody. -type V1CreateMigrationBody struct { - Name *string `json:"name,omitempty"` - Query string `json:"query"` - Rollback *string `json:"rollback,omitempty"` -} - -// V1CreateProjectBody defines model for V1CreateProjectBody. -type V1CreateProjectBody struct { - // DbPass Database password - DbPass string `json:"db_pass"` - - // DesiredInstanceSize Desired instance size. Omit this field to always default to the smallest possible size. - DesiredInstanceSize *V1CreateProjectBodyDesiredInstanceSize `json:"desired_instance_size,omitempty"` - - // HighAvailability [Experimental] Whether to enable high availability for the project. - HighAvailability *bool `json:"high_availability,omitempty"` - - // KpsEnabled This field is deprecated and is ignored in this request - // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set - KpsEnabled *bool `json:"kps_enabled,omitempty"` - - // Name Name of your project - Name string `json:"name"` - - // OrganizationId Deprecated: Use `organization_slug` instead. - // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set - OrganizationId *string `json:"organization_id,omitempty"` - - // OrganizationSlug Organization slug - OrganizationSlug string `json:"organization_slug"` - - // Plan Subscription Plan is now set on organization level and is ignored in this request - // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set - Plan *V1CreateProjectBodyPlan `json:"plan,omitempty"` - - // Region Region you want your server to reside in. Use region_selection instead. - // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set - Region *V1CreateProjectBodyRegion `json:"region,omitempty"` - - // RegionSelection Region selection. Only one of region or region_selection can be specified. - RegionSelection *V1CreateProjectBody_RegionSelection `json:"region_selection,omitempty"` - - // TemplateUrl Template URL used to create the project from the CLI. - TemplateUrl *string `json:"template_url,omitempty"` -} - -// V1CreateProjectBodyDesiredInstanceSize Desired instance size. Omit this field to always default to the smallest possible size. -type V1CreateProjectBodyDesiredInstanceSize string - -// V1CreateProjectBodyPlan Subscription Plan is now set on organization level and is ignored in this request -type V1CreateProjectBodyPlan string - -// V1CreateProjectBodyRegion Region you want your server to reside in. Use region_selection instead. -type V1CreateProjectBodyRegion string - -// V1CreateProjectBodyRegionSelection0 defines model for . -type V1CreateProjectBodyRegionSelection0 struct { - // Code Specific region code. The codes supported are not a stable API, and should be retrieved from the /available-regions endpoint. - Code V1CreateProjectBodyRegionSelection0Code `json:"code"` - Type V1CreateProjectBodyRegionSelection0Type `json:"type"` -} - -// V1CreateProjectBodyRegionSelection0Code Specific region code. The codes supported are not a stable API, and should be retrieved from the /available-regions endpoint. -type V1CreateProjectBodyRegionSelection0Code string - -// V1CreateProjectBodyRegionSelection0Type defines model for V1CreateProjectBody.RegionSelection.0.Type. -type V1CreateProjectBodyRegionSelection0Type string - -// V1CreateProjectBodyRegionSelection1 defines model for . -type V1CreateProjectBodyRegionSelection1 struct { - // Code The Smart Region Group's code. The codes supported are not a stable API, and should be retrieved from the /available-regions endpoint. - Code V1CreateProjectBodyRegionSelection1Code `json:"code"` - Type V1CreateProjectBodyRegionSelection1Type `json:"type"` -} - -// V1CreateProjectBodyRegionSelection1Code The Smart Region Group's code. The codes supported are not a stable API, and should be retrieved from the /available-regions endpoint. -type V1CreateProjectBodyRegionSelection1Code string - -// V1CreateProjectBodyRegionSelection1Type defines model for V1CreateProjectBody.RegionSelection.1.Type. -type V1CreateProjectBodyRegionSelection1Type string - -// V1CreateProjectBody_RegionSelection Region selection. Only one of region or region_selection can be specified. -type V1CreateProjectBody_RegionSelection struct { - union json.RawMessage -} - -// V1GetMigrationResponseOutput defines model for V1GetMigrationResponse_Output. -type V1GetMigrationResponseOutput struct { - CreatedBy *string `json:"created_by,omitempty"` - IdempotencyKey *string `json:"idempotency_key,omitempty"` - Name *string `json:"name,omitempty"` - Rollback *[]string `json:"rollback,omitempty"` - Statements *[]string `json:"statements,omitempty"` - Version string `json:"version"` -} - -// V1GetUsageApiCountResponseOutput defines model for V1GetUsageApiCountResponse_Output. -type V1GetUsageApiCountResponseOutput struct { - Error *V1GetUsageApiCountResponseOutput_Error `json:"error,omitempty"` - Result *[]struct { - Timestamp time.Time `json:"timestamp"` - TotalAuthRequests float32 `json:"total_auth_requests"` - TotalRealtimeRequests float32 `json:"total_realtime_requests"` - TotalRestRequests float32 `json:"total_rest_requests"` - TotalStorageRequests float32 `json:"total_storage_requests"` - } `json:"result,omitempty"` -} - -// V1GetUsageApiCountResponseOutputError0 defines model for . -type V1GetUsageApiCountResponseOutputError0 = string - -// V1GetUsageApiCountResponseOutputError1 defines model for . -type V1GetUsageApiCountResponseOutputError1 struct { - Code float32 `json:"code"` - Errors []struct { - Domain string `json:"domain"` - Location string `json:"location"` - LocationType string `json:"locationType"` - Message string `json:"message"` - Reason string `json:"reason"` - } `json:"errors"` - Message string `json:"message"` - Status string `json:"status"` -} - -// V1GetUsageApiCountResponseOutput_Error defines model for V1GetUsageApiCountResponseOutput.Error. -type V1GetUsageApiCountResponseOutput_Error struct { - union json.RawMessage -} - -// V1GetUsageApiRequestsCountResponseOutput defines model for V1GetUsageApiRequestsCountResponse_Output. -type V1GetUsageApiRequestsCountResponseOutput struct { - Error *V1GetUsageApiRequestsCountResponseOutput_Error `json:"error,omitempty"` - Result *[]struct { - Count float32 `json:"count"` - } `json:"result,omitempty"` -} - -// V1GetUsageApiRequestsCountResponseOutputError0 defines model for . -type V1GetUsageApiRequestsCountResponseOutputError0 = string - -// V1GetUsageApiRequestsCountResponseOutputError1 defines model for . -type V1GetUsageApiRequestsCountResponseOutputError1 struct { - Code float32 `json:"code"` - Errors []struct { - Domain string `json:"domain"` - Location string `json:"location"` - LocationType string `json:"locationType"` - Message string `json:"message"` - Reason string `json:"reason"` - } `json:"errors"` - Message string `json:"message"` - Status string `json:"status"` -} - -// V1GetUsageApiRequestsCountResponseOutput_Error defines model for V1GetUsageApiRequestsCountResponseOutput.Error. -type V1GetUsageApiRequestsCountResponseOutput_Error struct { - union json.RawMessage -} - -// V1ListEntitlementsResponseOutput defines model for V1ListEntitlementsResponse_Output. -type V1ListEntitlementsResponseOutput struct { - Entitlements []struct { - Config V1ListEntitlementsResponseOutput_Entitlements_Config `json:"config"` - Feature struct { - Key V1ListEntitlementsResponseOutputEntitlementsFeatureKey `json:"key"` - Type V1ListEntitlementsResponseOutputEntitlementsFeatureType `json:"type"` - } `json:"feature"` - HasAccess bool `json:"hasAccess"` - Type V1ListEntitlementsResponseOutputEntitlementsType `json:"type"` - } `json:"entitlements"` -} - -// V1ListEntitlementsResponseOutputEntitlementsConfig0 defines model for . -type V1ListEntitlementsResponseOutputEntitlementsConfig0 struct { - Enabled bool `json:"enabled"` -} - -// V1ListEntitlementsResponseOutputEntitlementsConfig1 defines model for . -type V1ListEntitlementsResponseOutputEntitlementsConfig1 struct { - Enabled bool `json:"enabled"` - Unit string `json:"unit"` - Unlimited bool `json:"unlimited"` - Value float32 `json:"value"` -} - -// V1ListEntitlementsResponseOutputEntitlementsConfig2 defines model for . -type V1ListEntitlementsResponseOutputEntitlementsConfig2 struct { - Enabled bool `json:"enabled"` - Set []string `json:"set"` -} - -// V1ListEntitlementsResponseOutput_Entitlements_Config defines model for V1ListEntitlementsResponseOutput.Entitlements.Config. -type V1ListEntitlementsResponseOutput_Entitlements_Config struct { - union json.RawMessage -} - -// V1ListEntitlementsResponseOutputEntitlementsFeatureKey defines model for V1ListEntitlementsResponseOutput.Entitlements.Feature.Key. -type V1ListEntitlementsResponseOutputEntitlementsFeatureKey string - -// V1ListEntitlementsResponseOutputEntitlementsFeatureType defines model for V1ListEntitlementsResponseOutput.Entitlements.Feature.Type. -type V1ListEntitlementsResponseOutputEntitlementsFeatureType string - -// V1ListEntitlementsResponseOutputEntitlementsType defines model for V1ListEntitlementsResponseOutput.Entitlements.Type. -type V1ListEntitlementsResponseOutputEntitlementsType string - -// V1ListMigrationsResponseOutput defines model for V1ListMigrationsResponse_Output. -type V1ListMigrationsResponseOutput = []struct { - Name *string `json:"name,omitempty"` - Version string `json:"version"` -} - -// V1OrganizationMemberResponseOutput defines model for V1OrganizationMemberResponse_Output. -type V1OrganizationMemberResponseOutput struct { - AvatarUrl nullable.Nullable[string] `json:"avatar_url"` - Email *string `json:"email,omitempty"` - MfaEnabled bool `json:"mfa_enabled"` - RoleName *string `json:"role_name,omitempty"` - UserId string `json:"user_id"` - UserName string `json:"user_name"` -} - -// V1OrganizationSlugResponseOutput defines model for V1OrganizationSlugResponse_Output. -type V1OrganizationSlugResponseOutput struct { - AllowedReleaseChannels []V1OrganizationSlugResponseOutputAllowedReleaseChannels `json:"allowed_release_channels"` - Id string `json:"id"` - Name string `json:"name"` - OptInTags []interface{} `json:"opt_in_tags"` - Plan *V1OrganizationSlugResponseOutputPlan `json:"plan,omitempty"` -} - -// V1OrganizationSlugResponseOutputAllowedReleaseChannels defines model for V1OrganizationSlugResponseOutput.AllowedReleaseChannels. -type V1OrganizationSlugResponseOutputAllowedReleaseChannels string - -// V1OrganizationSlugResponseOutputPlan defines model for V1OrganizationSlugResponseOutput.Plan. -type V1OrganizationSlugResponseOutputPlan string - -// V1PatchMigrationBody defines model for V1PatchMigrationBody. -type V1PatchMigrationBody struct { - Name *string `json:"name,omitempty"` - Rollback *string `json:"rollback,omitempty"` -} - -// V1PgbouncerConfigResponseOutput defines model for V1PgbouncerConfigResponse_Output. -type V1PgbouncerConfigResponseOutput struct { - ConnectionString *string `json:"connection_string,omitempty"` - DefaultPoolSize *int `json:"default_pool_size,omitempty"` - IgnoreStartupParameters *string `json:"ignore_startup_parameters,omitempty"` - MaxClientConn *int `json:"max_client_conn,omitempty"` - PoolMode *V1PgbouncerConfigResponseOutputPoolMode `json:"pool_mode,omitempty"` - QueryWaitTimeout *int `json:"query_wait_timeout,omitempty"` - ReservePoolSize *int `json:"reserve_pool_size,omitempty"` - ServerIdleTimeout *int `json:"server_idle_timeout,omitempty"` - ServerLifetime *int `json:"server_lifetime,omitempty"` -} - -// V1PgbouncerConfigResponseOutputPoolMode defines model for V1PgbouncerConfigResponseOutput.PoolMode. -type V1PgbouncerConfigResponseOutputPoolMode string - -// V1PostgrestConfigResponseOutput defines model for V1PostgrestConfigResponse_Output. -type V1PostgrestConfigResponseOutput struct { - DbExtraSearchPath string `json:"db_extra_search_path"` - - // DbPool If `null`, the value is automatically configured based on compute size. - DbPool nullable.Nullable[int] `json:"db_pool"` - - // DbPoolAcquisitionTimeout If `null`, the value is automatically configured to 10. - DbPoolAcquisitionTimeout nullable.Nullable[int] `json:"db_pool_acquisition_timeout"` - - // DbPoolMaxIdletime If `null`, the value is automatically configured to 30. - DbPoolMaxIdletime nullable.Nullable[int] `json:"db_pool_max_idletime"` - - // DbPoolMaxLifetime If `null`, the value is automatically configured to 1800. - DbPoolMaxLifetime nullable.Nullable[int] `json:"db_pool_max_lifetime"` - DbSchema string `json:"db_schema"` - MaxRows int `json:"max_rows"` -} - -// V1ProfileResponseOutput defines model for V1ProfileResponse_Output. -type V1ProfileResponseOutput struct { - GotrueId string `json:"gotrue_id"` - PrimaryEmail string `json:"primary_email"` - Username string `json:"username"` -} - -// V1ProjectAdvisorsResponseOutput defines model for V1ProjectAdvisorsResponse_Output. -type V1ProjectAdvisorsResponseOutput struct { - Lints []struct { - CacheKey string `json:"cache_key"` - Categories []V1ProjectAdvisorsResponseOutputLintsCategories `json:"categories"` - Description string `json:"description"` - Detail string `json:"detail"` - Facing V1ProjectAdvisorsResponseOutputLintsFacing `json:"facing"` - Level V1ProjectAdvisorsResponseOutputLintsLevel `json:"level"` - Metadata *struct { - Entity *string `json:"entity,omitempty"` - FkeyColumns *[]float32 `json:"fkey_columns,omitempty"` - FkeyName *string `json:"fkey_name,omitempty"` - Name *string `json:"name,omitempty"` - Schema *string `json:"schema,omitempty"` - Type *V1ProjectAdvisorsResponseOutputLintsMetadataType `json:"type,omitempty"` - } `json:"metadata,omitempty"` - Name V1ProjectAdvisorsResponseOutputLintsName `json:"name"` - ObservedAt *time.Time `json:"observed_at,omitempty"` - Remediation string `json:"remediation"` - Title string `json:"title"` - } `json:"lints"` -} - -// V1ProjectAdvisorsResponseOutputLintsCategories defines model for V1ProjectAdvisorsResponseOutput.Lints.Categories. -type V1ProjectAdvisorsResponseOutputLintsCategories string - -// V1ProjectAdvisorsResponseOutputLintsFacing defines model for V1ProjectAdvisorsResponseOutput.Lints.Facing. -type V1ProjectAdvisorsResponseOutputLintsFacing string - -// V1ProjectAdvisorsResponseOutputLintsLevel defines model for V1ProjectAdvisorsResponseOutput.Lints.Level. -type V1ProjectAdvisorsResponseOutputLintsLevel string - -// V1ProjectAdvisorsResponseOutputLintsMetadataType defines model for V1ProjectAdvisorsResponseOutput.Lints.Metadata.Type. -type V1ProjectAdvisorsResponseOutputLintsMetadataType string - -// V1ProjectAdvisorsResponseOutputLintsName defines model for V1ProjectAdvisorsResponseOutput.Lints.Name. -type V1ProjectAdvisorsResponseOutputLintsName string - -// V1ProjectRefResponseOutput defines model for V1ProjectRefResponse_Output. -type V1ProjectRefResponseOutput struct { - Id int `json:"id"` - Name string `json:"name"` - Ref string `json:"ref"` -} - -// V1ProjectResponseOutput defines model for V1ProjectResponse_Output. -type V1ProjectResponseOutput struct { - // CreatedAt Creation timestamp - CreatedAt string `json:"created_at"` - - // Id Deprecated: Use `ref` instead. - // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set - Id string `json:"id"` - - // Name Name of your project - Name string `json:"name"` - - // OrganizationId Deprecated: Use `organization_slug` instead. - // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set - OrganizationId string `json:"organization_id"` - - // OrganizationSlug Organization slug - OrganizationSlug string `json:"organization_slug"` - - // Ref Project ref - Ref string `json:"ref"` - - // Region Region of your project - Region string `json:"region"` - Status V1ProjectResponseOutputStatus `json:"status"` -} - -// V1ProjectResponseOutputStatus defines model for V1ProjectResponseOutput.Status. -type V1ProjectResponseOutputStatus string - -// V1ProjectWithDatabaseResponseOutput defines model for V1ProjectWithDatabaseResponse_Output. -type V1ProjectWithDatabaseResponseOutput struct { - // CreatedAt Creation timestamp - CreatedAt string `json:"created_at"` - Database struct { - // Host Database host - Host string `json:"host"` - - // PostgresEngine Database engine - PostgresEngine string `json:"postgres_engine"` - - // ReleaseChannel Release channel - ReleaseChannel string `json:"release_channel"` - - // Version Database version - Version string `json:"version"` - } `json:"database"` - - // Id Deprecated: Use `ref` instead. - // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set - Id string `json:"id"` - - // Name Name of your project - Name string `json:"name"` - - // OrganizationId Deprecated: Use `organization_slug` instead. - // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set - OrganizationId string `json:"organization_id"` - - // OrganizationSlug Organization slug - OrganizationSlug string `json:"organization_slug"` - - // Ref Project ref - Ref string `json:"ref"` - - // Region Region of your project - Region string `json:"region"` - Status V1ProjectWithDatabaseResponseOutputStatus `json:"status"` -} - -// V1ProjectWithDatabaseResponseOutputStatus defines model for V1ProjectWithDatabaseResponseOutput.Status. -type V1ProjectWithDatabaseResponseOutputStatus string - -// V1ReadOnlyQueryBody defines model for V1ReadOnlyQueryBody. -type V1ReadOnlyQueryBody struct { - Parameters *[]interface{} `json:"parameters,omitempty"` - Query string `json:"query"` -} - -// V1RestoreBackupBody defines model for V1RestoreBackupBody. -type V1RestoreBackupBody struct { - Id int `json:"id"` -} - -// V1RestorePitrBody defines model for V1RestorePitrBody. -type V1RestorePitrBody struct { - RecoveryTimeTargetUnix int64 `json:"recovery_time_target_unix"` -} - -// V1RestorePointPostBody defines model for V1RestorePointPostBody. -type V1RestorePointPostBody struct { - Name string `json:"name"` -} - -// V1RestorePointResponse defines model for V1RestorePointResponse. -type V1RestorePointResponse struct { - CompletedOn nullable.Nullable[time.Time] `json:"completed_on"` - Name string `json:"name"` - Status V1RestorePointResponseStatus `json:"status"` -} - -// V1RestorePointResponseStatus defines model for V1RestorePointResponse.Status. -type V1RestorePointResponseStatus string - -// V1RunQueryBody defines model for V1RunQueryBody. -type V1RunQueryBody struct { - Parameters *[]interface{} `json:"parameters,omitempty"` - Query string `json:"query"` - ReadOnly *bool `json:"read_only,omitempty"` -} - -// V1ServiceHealthResponseOutput defines model for V1ServiceHealthResponse_Output. -type V1ServiceHealthResponseOutput struct { - Error *string `json:"error,omitempty"` - - // Healthy Deprecated. Use `status` instead. - // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set - Healthy bool `json:"healthy"` - Info *V1ServiceHealthResponseOutput_Info `json:"info,omitempty"` - Name V1ServiceHealthResponseOutputName `json:"name"` - Status V1ServiceHealthResponseOutputStatus `json:"status"` -} - -// V1ServiceHealthResponseOutputInfo0 defines model for . -type V1ServiceHealthResponseOutputInfo0 struct { - Description string `json:"description"` - Name V1ServiceHealthResponseOutputInfo0Name `json:"name"` - Version string `json:"version"` -} - -// V1ServiceHealthResponseOutputInfo0Name defines model for V1ServiceHealthResponseOutput.Info.0.Name. -type V1ServiceHealthResponseOutputInfo0Name string - -// V1ServiceHealthResponseOutputInfo1 defines model for . -type V1ServiceHealthResponseOutputInfo1 struct { - ConnectedCluster int `json:"connected_cluster"` - DbConnected bool `json:"db_connected"` - - // Healthy Deprecated. Use `status` instead. - // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set - Healthy bool `json:"healthy"` - ReplicationConnected bool `json:"replication_connected"` -} - -// V1ServiceHealthResponseOutputInfo2 defines model for . -type V1ServiceHealthResponseOutputInfo2 struct { - DbSchema string `json:"db_schema"` -} - -// V1ServiceHealthResponseOutput_Info defines model for V1ServiceHealthResponseOutput.Info. -type V1ServiceHealthResponseOutput_Info struct { - union json.RawMessage -} - -// V1ServiceHealthResponseOutputName defines model for V1ServiceHealthResponseOutput.Name. -type V1ServiceHealthResponseOutputName string - -// V1ServiceHealthResponseOutputStatus defines model for V1ServiceHealthResponseOutput.Status. -type V1ServiceHealthResponseOutputStatus string - -// V1StorageBucketResponseOutput defines model for V1StorageBucketResponse_Output. -type V1StorageBucketResponseOutput struct { - CreatedAt string `json:"created_at"` - Id string `json:"id"` - Name string `json:"name"` - Owner string `json:"owner"` - Public bool `json:"public"` - UpdatedAt string `json:"updated_at"` -} - -// V1UndoBody defines model for V1UndoBody. -type V1UndoBody struct { - Name string `json:"name"` -} - -// V1UpdateBackupScheduleBody defines model for V1UpdateBackupScheduleBody. -type V1UpdateBackupScheduleBody struct { - // ScheduleFor Time of day to schedule daily backups, in UTC. Format: HH:MM:SS. - ScheduleFor string `json:"schedule_for"` -} - -// V1UpdateFunctionBodyEszip Raw Brotli-compressed ESZIP binary file. -// -// **Validation requirements** - The file must start with the four magic bytes `EZBR` (hex: `45 5A 42 52`). -// - The `ezbr_sha256` query parameter must contain the SHA-256 hash of the uploaded ESZIP payload. -// -// **Example** -// ```javascript -// import { readFile } from 'node:fs/promises'; -// import { brotliCompress } from 'node:zlib'; -// import { promisify } from 'node:util'; -// import { createHash } from 'node:crypto'; -// -// const compress = promisify(brotliCompress); -// const marker = Buffer.from('EZBR', 'ascii'); -// -// const file = await readFile('./function.eszip'); -// const compressed = await compress(file); -// -// const payload = Buffer.concat([marker, compressed]); -// -// const ezbr_sha256 = createHash('sha256') -// -// .update(payload) -// .digest('hex'); -// -// console.log(ezbr_sha256); -// ``` -type V1UpdateFunctionBodyEszip = openapi_types.File - -// V1UpdateFunctionBodyJson defines model for V1UpdateFunctionBodyJson. -type V1UpdateFunctionBodyJson struct { - Body *string `json:"body,omitempty"` - Name *string `json:"name,omitempty"` - VerifyJwt *bool `json:"verify_jwt,omitempty"` -} - -// V1UpdatePasswordBody defines model for V1UpdatePasswordBody. -type V1UpdatePasswordBody struct { - Password string `json:"password"` -} - -// V1UpdatePasswordResponseOutput defines model for V1UpdatePasswordResponse_Output. -type V1UpdatePasswordResponseOutput struct { - Message string `json:"message"` -} - -// V1UpdatePostgrestConfigBody defines model for V1UpdatePostgrestConfigBody. -type V1UpdatePostgrestConfigBody struct { - DbExtraSearchPath *string `json:"db_extra_search_path,omitempty"` - DbPool *int `json:"db_pool,omitempty"` - DbPoolAcquisitionTimeout *int `json:"db_pool_acquisition_timeout,omitempty"` - DbPoolMaxIdletime *int `json:"db_pool_max_idletime,omitempty"` - DbPoolMaxLifetime *int `json:"db_pool_max_lifetime,omitempty"` - DbSchema *string `json:"db_schema,omitempty"` - MaxRows *int `json:"max_rows,omitempty"` -} - -// V1UpdateProjectBody defines model for V1UpdateProjectBody. -type V1UpdateProjectBody struct { - Name string `json:"name"` -} - -// V1UpsertMigrationBody defines model for V1UpsertMigrationBody. -type V1UpsertMigrationBody struct { - Name *string `json:"name,omitempty"` - Query string `json:"query"` - Rollback *string `json:"rollback,omitempty"` -} - -// VanitySubdomainBody defines model for VanitySubdomainBody. -type VanitySubdomainBody struct { - VanitySubdomain string `json:"vanity_subdomain"` -} - -// VanitySubdomainConfigResponseOutput defines model for VanitySubdomainConfigResponse_Output. -type VanitySubdomainConfigResponseOutput struct { - CustomDomain *string `json:"custom_domain,omitempty"` - Status VanitySubdomainConfigResponseOutputStatus `json:"status"` -} - -// VanitySubdomainConfigResponseOutputStatus defines model for VanitySubdomainConfigResponseOutput.Status. -type VanitySubdomainConfigResponseOutputStatus string - -// bearerContextKey is the context key for bearer security scheme -type bearerContextKey string - -// oauth2ContextKey is the context key for oauth2 security scheme -type oauth2ContextKey string - -// V1DeleteABranchParams defines parameters for V1DeleteABranch. -type V1DeleteABranchParams struct { - // Force If set to false, schedule deletion with 1-hour grace period (only when soft deletion is enabled). - Force *string `form:"force,omitempty" json:"force,omitempty"` -} - -// V1DiffABranchParams defines parameters for V1DiffABranch. -type V1DiffABranchParams struct { - IncludedSchemas *string `form:"included_schemas,omitempty" json:"included_schemas,omitempty"` - - // Pgdelta Use pg-delta instead of Migra for diffing when true. - // Boolean string. - // - // Truthy values: `true`, `1`, `yes`, `on`, `y`, `enabled` - // - // Falsy values: `false`, `0`, `no`, `off`, `n`, `disabled` - Pgdelta *string `form:"pgdelta,omitempty" json:"pgdelta,omitempty"` -} - -// V1AuthorizeUserParams defines parameters for V1AuthorizeUser. -type V1AuthorizeUserParams struct { - ClientId openapi_types.UUID `form:"client_id" json:"client_id"` - ResponseType V1AuthorizeUserParamsResponseType `form:"response_type" json:"response_type"` - RedirectUri string `form:"redirect_uri" json:"redirect_uri"` - Scope *string `form:"scope,omitempty" json:"scope,omitempty"` - State *string `form:"state,omitempty" json:"state,omitempty"` - ResponseMode *string `form:"response_mode,omitempty" json:"response_mode,omitempty"` - CodeChallenge *string `form:"code_challenge,omitempty" json:"code_challenge,omitempty"` - CodeChallengeMethod *V1AuthorizeUserParamsCodeChallengeMethod `form:"code_challenge_method,omitempty" json:"code_challenge_method,omitempty"` - - // OrganizationSlug Organization slug - OrganizationSlug *string `form:"organization_slug,omitempty" json:"organization_slug,omitempty"` - TargetFlow *string `form:"target_flow,omitempty" json:"target_flow,omitempty"` - - // Resource Resource indicator for MCP (Model Context Protocol) clients - Resource *string `form:"resource,omitempty" json:"resource,omitempty"` -} - -// V1AuthorizeUserParamsResponseType defines parameters for V1AuthorizeUser. -type V1AuthorizeUserParamsResponseType string - -// V1AuthorizeUserParamsCodeChallengeMethod defines parameters for V1AuthorizeUser. -type V1AuthorizeUserParamsCodeChallengeMethod string - -// V1OauthAuthorizeProjectClaimParams defines parameters for V1OauthAuthorizeProjectClaim. -type V1OauthAuthorizeProjectClaimParams struct { - // ProjectRef Project ref - ProjectRef string `form:"project_ref" json:"project_ref"` - ClientId openapi_types.UUID `form:"client_id" json:"client_id"` - ResponseType V1OauthAuthorizeProjectClaimParamsResponseType `form:"response_type" json:"response_type"` - RedirectUri string `form:"redirect_uri" json:"redirect_uri"` - State *string `form:"state,omitempty" json:"state,omitempty"` - ResponseMode *string `form:"response_mode,omitempty" json:"response_mode,omitempty"` - CodeChallenge *string `form:"code_challenge,omitempty" json:"code_challenge,omitempty"` - CodeChallengeMethod *V1OauthAuthorizeProjectClaimParamsCodeChallengeMethod `form:"code_challenge_method,omitempty" json:"code_challenge_method,omitempty"` -} - -// V1OauthAuthorizeProjectClaimParamsResponseType defines parameters for V1OauthAuthorizeProjectClaim. -type V1OauthAuthorizeProjectClaimParamsResponseType string - -// V1OauthAuthorizeProjectClaimParamsCodeChallengeMethod defines parameters for V1OauthAuthorizeProjectClaim. -type V1OauthAuthorizeProjectClaimParamsCodeChallengeMethod string - -// V1GetAllProjectsForOrganizationParams defines parameters for V1GetAllProjectsForOrganization. -type V1GetAllProjectsForOrganizationParams struct { - // Offset Number of projects to skip - Offset *int `form:"offset,omitempty" json:"offset,omitempty"` - - // Limit Number of projects to return per page - Limit *int `form:"limit,omitempty" json:"limit,omitempty"` - - // Search Search projects by name - Search *string `form:"search,omitempty" json:"search,omitempty"` - - // Sort Sort order for projects - Sort *V1GetAllProjectsForOrganizationParamsSort `form:"sort,omitempty" json:"sort,omitempty"` - - // Statuses A comma-separated list of project statuses to filter by. - // - // The following values are supported: `ACTIVE_HEALTHY`, `INACTIVE`. - Statuses *string `form:"statuses,omitempty" json:"statuses,omitempty"` -} - -// V1GetAllProjectsForOrganizationParamsSort defines parameters for V1GetAllProjectsForOrganization. -type V1GetAllProjectsForOrganizationParamsSort string - -// V1GetAvailableRegionsParams defines parameters for V1GetAvailableRegions. -type V1GetAvailableRegionsParams struct { - // OrganizationSlug Slug of your organization - OrganizationSlug string `form:"organization_slug" json:"organization_slug"` - - // Continent Continent code to determine regional recommendations: NA (North America), SA (South America), EU (Europe), AF (Africa), AS (Asia), OC (Oceania), AN (Antarctica) - Continent *V1GetAvailableRegionsParamsContinent `form:"continent,omitempty" json:"continent,omitempty"` - - // DesiredInstanceSize Desired instance size. Omit this field to always default to the smallest possible size. - DesiredInstanceSize *V1GetAvailableRegionsParamsDesiredInstanceSize `form:"desired_instance_size,omitempty" json:"desired_instance_size,omitempty"` -} - -// V1GetAvailableRegionsParamsContinent defines parameters for V1GetAvailableRegions. -type V1GetAvailableRegionsParamsContinent string - -// V1GetAvailableRegionsParamsDesiredInstanceSize defines parameters for V1GetAvailableRegions. -type V1GetAvailableRegionsParamsDesiredInstanceSize string - -// V1ListActionRunsParams defines parameters for V1ListActionRuns. -type V1ListActionRunsParams struct { - Offset *float32 `form:"offset,omitempty" json:"offset,omitempty"` - Limit *float32 `form:"limit,omitempty" json:"limit,omitempty"` -} - -// V1GetSecurityAdvisorsParams defines parameters for V1GetSecurityAdvisors. -type V1GetSecurityAdvisorsParams struct { - LintType *V1GetSecurityAdvisorsParamsLintType `form:"lint_type,omitempty" json:"lint_type,omitempty"` -} - -// V1GetSecurityAdvisorsParamsLintType defines parameters for V1GetSecurityAdvisors. -type V1GetSecurityAdvisorsParamsLintType string - -// V1GetProjectFunctionCombinedStatsParams defines parameters for V1GetProjectFunctionCombinedStats. -type V1GetProjectFunctionCombinedStatsParams struct { - Interval V1GetProjectFunctionCombinedStatsParamsInterval `form:"interval" json:"interval"` - FunctionId string `form:"function_id" json:"function_id"` -} - -// V1GetProjectFunctionCombinedStatsParamsInterval defines parameters for V1GetProjectFunctionCombinedStats. -type V1GetProjectFunctionCombinedStatsParamsInterval string - -// V1GetProjectLogsParams defines parameters for V1GetProjectLogs. -type V1GetProjectLogsParams struct { - // Sql Custom SQL query to execute on the logs. See [querying logs](https://supabase.com/docs/guides/monitoring-and-debugging/logs#querying-with-the-logs-explorer) for more details. - Sql *string `form:"sql,omitempty" json:"sql,omitempty"` - IsoTimestampStart *time.Time `form:"iso_timestamp_start,omitempty" json:"iso_timestamp_start,omitempty"` - IsoTimestampEnd *time.Time `form:"iso_timestamp_end,omitempty" json:"iso_timestamp_end,omitempty"` -} - -// V1GetProjectUsageApiCountParams defines parameters for V1GetProjectUsageApiCount. -type V1GetProjectUsageApiCountParams struct { - Interval *V1GetProjectUsageApiCountParamsInterval `form:"interval,omitempty" json:"interval,omitempty"` -} - -// V1GetProjectUsageApiCountParamsInterval defines parameters for V1GetProjectUsageApiCount. -type V1GetProjectUsageApiCountParamsInterval string - -// V1GetProjectApiKeysParams defines parameters for V1GetProjectApiKeys. -type V1GetProjectApiKeysParams struct { - // Reveal Boolean string. - // - // Truthy values: `true`, `1`, `yes`, `on`, `y`, `enabled` - // - // Falsy values: `false`, `0`, `no`, `off`, `n`, `disabled` - Reveal *string `form:"reveal,omitempty" json:"reveal,omitempty"` -} - -// V1CreateProjectApiKeyParams defines parameters for V1CreateProjectApiKey. -type V1CreateProjectApiKeyParams struct { - // Reveal Boolean string. - // - // Truthy values: `true`, `1`, `yes`, `on`, `y`, `enabled` - // - // Falsy values: `false`, `0`, `no`, `off`, `n`, `disabled` - Reveal *string `form:"reveal,omitempty" json:"reveal,omitempty"` -} - -// V1UpdateProjectLegacyApiKeysParams defines parameters for V1UpdateProjectLegacyApiKeys. -type V1UpdateProjectLegacyApiKeysParams struct { - // Enabled Boolean string. - // - // Truthy values: `true`, `1`, `yes`, `on`, `y`, `enabled` - // - // Falsy values: `false`, `0`, `no`, `off`, `n`, `disabled` - Enabled string `form:"enabled" json:"enabled"` -} - -// V1DeleteProjectApiKeyParams defines parameters for V1DeleteProjectApiKey. -type V1DeleteProjectApiKeyParams struct { - // Reveal Boolean string, true or false - Reveal *string `form:"reveal,omitempty" json:"reveal,omitempty"` - - // WasCompromised Boolean string, true or false - WasCompromised *string `form:"was_compromised,omitempty" json:"was_compromised,omitempty"` - Reason *string `form:"reason,omitempty" json:"reason,omitempty"` -} - -// V1GetProjectApiKeyParams defines parameters for V1GetProjectApiKey. -type V1GetProjectApiKeyParams struct { - // Reveal Boolean string. - // - // Truthy values: `true`, `1`, `yes`, `on`, `y`, `enabled` - // - // Falsy values: `false`, `0`, `no`, `off`, `n`, `disabled` - Reveal *string `form:"reveal,omitempty" json:"reveal,omitempty"` -} - -// V1UpdateProjectApiKeyParams defines parameters for V1UpdateProjectApiKey. -type V1UpdateProjectApiKeyParams struct { - // Reveal Boolean string. - // - // Truthy values: `true`, `1`, `yes`, `on`, `y`, `enabled` - // - // Falsy values: `false`, `0`, `no`, `off`, `n`, `disabled` - Reveal *string `form:"reveal,omitempty" json:"reveal,omitempty"` -} - -// V1RemoveProjectAddonParamsAddonVariant0 defines parameters for V1RemoveProjectAddon. -type V1RemoveProjectAddonParamsAddonVariant0 string - -// V1RemoveProjectAddonParamsAddonVariant1 defines parameters for V1RemoveProjectAddon. -type V1RemoveProjectAddonParamsAddonVariant1 string - -// V1RemoveProjectAddonParamsAddonVariant2 defines parameters for V1RemoveProjectAddon. -type V1RemoveProjectAddonParamsAddonVariant2 string - -// V1RemoveProjectAddonParamsAddonVariant3 defines parameters for V1RemoveProjectAddon. -type V1RemoveProjectAddonParamsAddonVariant3 string - -// V1DeleteHostnameConfigParams defines parameters for V1DeleteHostnameConfig. -type V1DeleteHostnameConfigParams struct { - // RemoveAddon If true, also removes the custom domain add-on from the project subscription. - RemoveAddon *string `form:"remove_addon,omitempty" json:"remove_addon,omitempty"` -} - -// V1GetRestorePointParams defines parameters for V1GetRestorePoint. -type V1GetRestorePointParams struct { - Name *string `form:"name,omitempty" json:"name,omitempty"` -} - -// V1RollbackMigrationsParams defines parameters for V1RollbackMigrations. -type V1RollbackMigrationsParams struct { - // Gte Rollback migrations greater or equal to this version - Gte string `form:"gte" json:"gte"` -} - -// V1ApplyAMigrationParams defines parameters for V1ApplyAMigration. -type V1ApplyAMigrationParams struct { - // IdempotencyKey A unique key to ensure the same migration is tracked only once. - IdempotencyKey *string `json:"Idempotency-Key,omitempty"` -} - -// V1UpsertAMigrationParams defines parameters for V1UpsertAMigration. -type V1UpsertAMigrationParams struct { - // IdempotencyKey A unique key to ensure the same migration is tracked only once. - IdempotencyKey *string `json:"Idempotency-Key,omitempty"` -} - -// V1GetDatabaseOpenapiParams defines parameters for V1GetDatabaseOpenapi. -type V1GetDatabaseOpenapiParams struct { - // Schema The database schema to generate the OpenAPI spec for - Schema *string `form:"schema,omitempty" json:"schema,omitempty"` -} - -// V1CreateAFunctionParams defines parameters for V1CreateAFunction. -type V1CreateAFunctionParams struct { - Slug *string `form:"slug,omitempty" json:"slug,omitempty"` - Name *string `form:"name,omitempty" json:"name,omitempty"` - VerifyJwt *string `form:"verify_jwt,omitempty" json:"verify_jwt,omitempty"` - ImportMap *string `form:"import_map,omitempty" json:"import_map,omitempty"` - EntrypointPath *string `form:"entrypoint_path,omitempty" json:"entrypoint_path,omitempty"` - ImportMapPath *string `form:"import_map_path,omitempty" json:"import_map_path,omitempty"` - EzbrSha256 *string `form:"ezbr_sha256,omitempty" json:"ezbr_sha256,omitempty"` -} - -// V1DeployAFunctionParams defines parameters for V1DeployAFunction. -type V1DeployAFunctionParams struct { - Slug *string `form:"slug,omitempty" json:"slug,omitempty"` - BundleOnly *string `form:"bundleOnly,omitempty" json:"bundleOnly,omitempty"` -} - -// V1UpdateAFunctionParams defines parameters for V1UpdateAFunction. -type V1UpdateAFunctionParams struct { - Slug *string `form:"slug,omitempty" json:"slug,omitempty"` - Name *string `form:"name,omitempty" json:"name,omitempty"` - VerifyJwt *string `form:"verify_jwt,omitempty" json:"verify_jwt,omitempty"` - ImportMap *string `form:"import_map,omitempty" json:"import_map,omitempty"` - EntrypointPath *string `form:"entrypoint_path,omitempty" json:"entrypoint_path,omitempty"` - ImportMapPath *string `form:"import_map_path,omitempty" json:"import_map_path,omitempty"` - EzbrSha256 *string `form:"ezbr_sha256,omitempty" json:"ezbr_sha256,omitempty"` -} - -// V1GetServicesHealthParams defines parameters for V1GetServicesHealth. -type V1GetServicesHealthParams struct { - // Services Comma-separated list of enums or array of enums. - Services []V1GetServicesHealthParamsServices `form:"services" json:"services"` - TimeoutMs *int `form:"timeout_ms,omitempty" json:"timeout_ms,omitempty"` -} - -// V1GetServicesHealthParamsServices defines parameters for V1GetServicesHealth. -type V1GetServicesHealthParamsServices string - -// V1GetJitAccessConfig200JSONResponseBody0 defines parameters for V1GetJitAccessConfig. -type V1GetJitAccessConfig200JSONResponseBody0 struct { - AppliedSuccessfully *bool `json:"appliedSuccessfully,omitempty"` - State V1GetJitAccessConfig200JSONResponseBody0State `json:"state"` -} - -// V1GetJitAccessConfig200JSONResponseBody0State defines parameters for V1GetJitAccessConfig. -type V1GetJitAccessConfig200JSONResponseBody0State string - -// V1GetJitAccessConfig200JSONResponseBody1 defines parameters for V1GetJitAccessConfig. -type V1GetJitAccessConfig200JSONResponseBody1 struct { - State string `json:"state"` - UnavailableReason V1GetJitAccessConfig200JSONResponseBody1UnavailableReason `json:"unavailableReason"` -} - -// V1GetJitAccessConfig200JSONResponseBody1UnavailableReason defines parameters for V1GetJitAccessConfig. -type V1GetJitAccessConfig200JSONResponseBody1UnavailableReason string - -// V1GetJitAccessConfig200JSONResponseBody defines parameters for V1GetJitAccessConfig. -type V1GetJitAccessConfig200JSONResponseBody struct { - union json.RawMessage -} - -// V1UpdateJitAccessConfig200JSONResponseBody0 defines parameters for V1UpdateJitAccessConfig. -type V1UpdateJitAccessConfig200JSONResponseBody0 struct { - AppliedSuccessfully *bool `json:"appliedSuccessfully,omitempty"` - State V1UpdateJitAccessConfig200JSONResponseBody0State `json:"state"` -} - -// V1UpdateJitAccessConfig200JSONResponseBody0State defines parameters for V1UpdateJitAccessConfig. -type V1UpdateJitAccessConfig200JSONResponseBody0State string - -// V1UpdateJitAccessConfig200JSONResponseBody1 defines parameters for V1UpdateJitAccessConfig. -type V1UpdateJitAccessConfig200JSONResponseBody1 struct { - State string `json:"state"` - UnavailableReason V1UpdateJitAccessConfig200JSONResponseBody1UnavailableReason `json:"unavailableReason"` -} - -// V1UpdateJitAccessConfig200JSONResponseBody1UnavailableReason defines parameters for V1UpdateJitAccessConfig. -type V1UpdateJitAccessConfig200JSONResponseBody1UnavailableReason string - -// V1UpdateJitAccessConfig200JSONResponseBody defines parameters for V1UpdateJitAccessConfig. -type V1UpdateJitAccessConfig200JSONResponseBody struct { - union json.RawMessage -} - -// V1GenerateTypescriptTypesParams defines parameters for V1GenerateTypescriptTypes. -type V1GenerateTypescriptTypesParams struct { - IncludedSchemas *string `form:"included_schemas,omitempty" json:"included_schemas,omitempty"` -} - -// V1GetPostgresUpgradeStatusParams defines parameters for V1GetPostgresUpgradeStatus. -type V1GetPostgresUpgradeStatusParams struct { - TrackingId *string `form:"tracking_id,omitempty" json:"tracking_id,omitempty"` -} - -// V1ListAllSnippetsParams defines parameters for V1ListAllSnippets. -type V1ListAllSnippetsParams struct { - // ProjectRef Project ref - ProjectRef *string `form:"project_ref,omitempty" json:"project_ref,omitempty"` - Cursor *string `form:"cursor,omitempty" json:"cursor,omitempty"` - Limit *string `form:"limit,omitempty" json:"limit,omitempty"` - SortBy *V1ListAllSnippetsParamsSortBy `form:"sort_by,omitempty" json:"sort_by,omitempty"` - SortOrder *V1ListAllSnippetsParamsSortOrder `form:"sort_order,omitempty" json:"sort_order,omitempty"` -} - -// V1ListAllSnippetsParamsSortBy defines parameters for V1ListAllSnippets. -type V1ListAllSnippetsParamsSortBy string - -// V1ListAllSnippetsParamsSortOrder defines parameters for V1ListAllSnippets. -type V1ListAllSnippetsParamsSortOrder string - -// V1UpdateABranchConfigJSONRequestBody defines body for V1UpdateABranchConfig for application/json ContentType. -type V1UpdateABranchConfigJSONRequestBody = UpdateBranchBody - -// V1MergeABranchJSONRequestBody defines body for V1MergeABranch for application/json ContentType. -type V1MergeABranchJSONRequestBody = BranchActionBody - -// V1PushABranchJSONRequestBody defines body for V1PushABranch for application/json ContentType. -type V1PushABranchJSONRequestBody = BranchActionBody - -// V1ResetABranchJSONRequestBody defines body for V1ResetABranch for application/json ContentType. -type V1ResetABranchJSONRequestBody = BranchActionBody - -// V1RevokeTokenJSONRequestBody defines body for V1RevokeToken for application/json ContentType. -type V1RevokeTokenJSONRequestBody = OAuthRevokeTokenBody - -// V1ExchangeOauthTokenFormdataRequestBody defines body for V1ExchangeOauthToken for application/x-www-form-urlencoded ContentType. -type V1ExchangeOauthTokenFormdataRequestBody = OAuthTokenBody - -// V1CreateAnOrganizationJSONRequestBody defines body for V1CreateAnOrganization for application/json ContentType. -type V1CreateAnOrganizationJSONRequestBody = CreateOrganizationV1 - -// V1CreateAProjectJSONRequestBody defines body for V1CreateAProject for application/json ContentType. -type V1CreateAProjectJSONRequestBody = V1CreateProjectBody - -// V1UpdateAProjectJSONRequestBody defines body for V1UpdateAProject for application/json ContentType. -type V1UpdateAProjectJSONRequestBody = V1UpdateProjectBody - -// V1UpdateActionRunStatusJSONRequestBody defines body for V1UpdateActionRunStatus for application/json ContentType. -type V1UpdateActionRunStatusJSONRequestBody = UpdateRunStatusBody - -// V1CreateProjectApiKeyJSONRequestBody defines body for V1CreateProjectApiKey for application/json ContentType. -type V1CreateProjectApiKeyJSONRequestBody = CreateApiKeyBody - -// V1UpdateProjectApiKeyJSONRequestBody defines body for V1UpdateProjectApiKey for application/json ContentType. -type V1UpdateProjectApiKeyJSONRequestBody = UpdateApiKeyBody - -// V1ApplyProjectAddonJSONRequestBody defines body for V1ApplyProjectAddon for application/json ContentType. -type V1ApplyProjectAddonJSONRequestBody = ApplyProjectAddonBody - -// V1CreateABranchJSONRequestBody defines body for V1CreateABranch for application/json ContentType. -type V1CreateABranchJSONRequestBody = CreateBranchBody - -// V1CreateLoginRoleJSONRequestBody defines body for V1CreateLoginRole for application/json ContentType. -type V1CreateLoginRoleJSONRequestBody = CreateRoleBody - -// V1UpdateAuthServiceConfigJSONRequestBody defines body for V1UpdateAuthServiceConfig for application/json ContentType. -type V1UpdateAuthServiceConfigJSONRequestBody = UpdateAuthConfigBody - -// V1CreateProjectSigningKeyJSONRequestBody defines body for V1CreateProjectSigningKey for application/json ContentType. -type V1CreateProjectSigningKeyJSONRequestBody = CreateSigningKeyBody - -// V1UpdateProjectSigningKeyJSONRequestBody defines body for V1UpdateProjectSigningKey for application/json ContentType. -type V1UpdateProjectSigningKeyJSONRequestBody = UpdateSigningKeyBody - -// V1CreateASsoProviderJSONRequestBody defines body for V1CreateASsoProvider for application/json ContentType. -type V1CreateASsoProviderJSONRequestBody = CreateProviderBody - -// V1UpdateASsoProviderJSONRequestBody defines body for V1UpdateASsoProvider for application/json ContentType. -type V1UpdateASsoProviderJSONRequestBody = UpdateProviderBody - -// V1CreateProjectTpaIntegrationJSONRequestBody defines body for V1CreateProjectTpaIntegration for application/json ContentType. -type V1CreateProjectTpaIntegrationJSONRequestBody = CreateThirdPartyAuthBody - -// V1UpdatePoolerConfigJSONRequestBody defines body for V1UpdatePoolerConfig for application/json ContentType. -type V1UpdatePoolerConfigJSONRequestBody = UpdateSupavisorConfigBody - -// V1UpdatePostgresConfigJSONRequestBody defines body for V1UpdatePostgresConfig for application/json ContentType. -type V1UpdatePostgresConfigJSONRequestBody = UpdatePostgresConfigBody - -// V1ModifyDatabaseDiskJSONRequestBody defines body for V1ModifyDatabaseDisk for application/json ContentType. -type V1ModifyDatabaseDiskJSONRequestBody = DiskRequestBody - -// V1UpdateRealtimeConfigJSONRequestBody defines body for V1UpdateRealtimeConfig for application/json ContentType. -type V1UpdateRealtimeConfigJSONRequestBody = UpdateRealtimeConfigBody - -// V1UpdateStorageConfigJSONRequestBody defines body for V1UpdateStorageConfig for application/json ContentType. -type V1UpdateStorageConfigJSONRequestBody = UpdateStorageConfigBody - -// V1UpdateHostnameConfigJSONRequestBody defines body for V1UpdateHostnameConfig for application/json ContentType. -type V1UpdateHostnameConfigJSONRequestBody = UpdateCustomHostnameBody - -// V1RestorePhysicalBackupJSONRequestBody defines body for V1RestorePhysicalBackup for application/json ContentType. -type V1RestorePhysicalBackupJSONRequestBody = V1RestoreBackupBody - -// V1RestorePitrBackupJSONRequestBody defines body for V1RestorePitrBackup for application/json ContentType. -type V1RestorePitrBackupJSONRequestBody = V1RestorePitrBody - -// V1CreateRestorePointJSONRequestBody defines body for V1CreateRestorePoint for application/json ContentType. -type V1CreateRestorePointJSONRequestBody = V1RestorePointPostBody - -// V1UpdateBackupScheduleJSONRequestBody defines body for V1UpdateBackupSchedule for application/json ContentType. -type V1UpdateBackupScheduleJSONRequestBody = V1UpdateBackupScheduleBody - -// V1UndoJSONRequestBody defines body for V1Undo for application/json ContentType. -type V1UndoJSONRequestBody = V1UndoBody - -// V1AuthorizeJitAccessJSONRequestBody defines body for V1AuthorizeJitAccess for application/json ContentType. -type V1AuthorizeJitAccessJSONRequestBody = AuthorizeJitAccessBody - -// V1UpdateJitAccessJSONRequestBody defines body for V1UpdateJitAccess for application/json ContentType. -type V1UpdateJitAccessJSONRequestBody = UpdateJitAccessBody - -// V1InviteExternalJitAccessJSONRequestBody defines body for V1InviteExternalJitAccess for application/json ContentType. -type V1InviteExternalJitAccessJSONRequestBody = InviteExternalUserJitAccessBody - -// V1AcceptInviteExternalJitAccessJSONRequestBody defines body for V1AcceptInviteExternalJitAccess for application/json ContentType. -type V1AcceptInviteExternalJitAccessJSONRequestBody = AcceptInviteExternalUserJitAccessBody - -// V1ApplyAMigrationJSONRequestBody defines body for V1ApplyAMigration for application/json ContentType. -type V1ApplyAMigrationJSONRequestBody = V1CreateMigrationBody - -// V1UpsertAMigrationJSONRequestBody defines body for V1UpsertAMigration for application/json ContentType. -type V1UpsertAMigrationJSONRequestBody = V1UpsertMigrationBody - -// V1PatchAMigrationJSONRequestBody defines body for V1PatchAMigration for application/json ContentType. -type V1PatchAMigrationJSONRequestBody = V1PatchMigrationBody - -// V1UpdateDatabasePasswordJSONRequestBody defines body for V1UpdateDatabasePassword for application/json ContentType. -type V1UpdateDatabasePasswordJSONRequestBody = V1UpdatePasswordBody - -// V1RunAQueryJSONRequestBody defines body for V1RunAQuery for application/json ContentType. -type V1RunAQueryJSONRequestBody = V1RunQueryBody - -// V1ReadOnlyQueryJSONRequestBody defines body for V1ReadOnlyQuery for application/json ContentType. -type V1ReadOnlyQueryJSONRequestBody = V1ReadOnlyQueryBody - -// V1CreateAFunctionJSONRequestBody defines body for V1CreateAFunction for application/json ContentType. -type V1CreateAFunctionJSONRequestBody = V1CreateFunctionBody - -// V1BulkUpdateFunctionsJSONRequestBody defines body for V1BulkUpdateFunctions for application/json ContentType. -type V1BulkUpdateFunctionsJSONRequestBody = BulkUpdateFunctionBody - -// V1DeployAFunctionMultipartRequestBody defines body for V1DeployAFunction for multipart/form-data ContentType. -type V1DeployAFunctionMultipartRequestBody = FunctionDeployBody - -// V1UpdateAFunctionJSONRequestBody defines body for V1UpdateAFunction for application/json ContentType. -type V1UpdateAFunctionJSONRequestBody = V1UpdateFunctionBodyJson - -// V1UpdateJitAccessConfigJSONRequestBody defines body for V1UpdateJitAccessConfig for application/json ContentType. -type V1UpdateJitAccessConfigJSONRequestBody = JitAccessRequestRequest - -// V1DeleteNetworkBansJSONRequestBody defines body for V1DeleteNetworkBans for application/json ContentType. -type V1DeleteNetworkBansJSONRequestBody = RemoveNetworkBanRequest - -// V1PatchNetworkRestrictionsJSONRequestBody defines body for V1PatchNetworkRestrictions for application/json ContentType. -type V1PatchNetworkRestrictionsJSONRequestBody = NetworkRestrictionsPatchRequest - -// V1UpdateNetworkRestrictionsJSONRequestBody defines body for V1UpdateNetworkRestrictions for application/json ContentType. -type V1UpdateNetworkRestrictionsJSONRequestBody = NetworkRestrictionsRequest - -// V1UpdatePgsodiumConfigJSONRequestBody defines body for V1UpdatePgsodiumConfig for application/json ContentType. -type V1UpdatePgsodiumConfigJSONRequestBody = UpdatePgsodiumConfigBody - -// V1UpdatePostgrestServiceConfigJSONRequestBody defines body for V1UpdatePostgrestServiceConfig for application/json ContentType. -type V1UpdatePostgrestServiceConfigJSONRequestBody = V1UpdatePostgrestConfigBody - -// V1RemoveAReadReplicaJSONRequestBody defines body for V1RemoveAReadReplica for application/json ContentType. -type V1RemoveAReadReplicaJSONRequestBody = RemoveReadReplicaBody - -// V1SetupAReadReplicaJSONRequestBody defines body for V1SetupAReadReplica for application/json ContentType. -type V1SetupAReadReplicaJSONRequestBody = SetUpReadReplicaBody - -// V1BulkDeleteSecretsJSONRequestBody defines body for V1BulkDeleteSecrets for application/json ContentType. -type V1BulkDeleteSecretsJSONRequestBody = DeleteSecretsBody - -// V1BulkCreateSecretsJSONRequestBody defines body for V1BulkCreateSecrets for application/json ContentType. -type V1BulkCreateSecretsJSONRequestBody = CreateSecretBody - -// V1UpdateSslEnforcementConfigJSONRequestBody defines body for V1UpdateSslEnforcementConfig for application/json ContentType. -type V1UpdateSslEnforcementConfigJSONRequestBody = SslEnforcementRequest - -// V1UpgradePostgresVersionJSONRequestBody defines body for V1UpgradePostgresVersion for application/json ContentType. -type V1UpgradePostgresVersionJSONRequestBody = UpgradeDatabaseBody - -// V1ActivateVanitySubdomainConfigJSONRequestBody defines body for V1ActivateVanitySubdomainConfig for application/json ContentType. -type V1ActivateVanitySubdomainConfigJSONRequestBody = VanitySubdomainBody - -// V1CheckVanitySubdomainAvailabilityJSONRequestBody defines body for V1CheckVanitySubdomainAvailability for application/json ContentType. -type V1CheckVanitySubdomainAvailabilityJSONRequestBody = VanitySubdomainBody - -// AsAnalyticsResponseOutputError0 returns the union data inside the AnalyticsResponseOutput_Error as a AnalyticsResponseOutputError0 -func (t AnalyticsResponseOutput_Error) AsAnalyticsResponseOutputError0() (AnalyticsResponseOutputError0, error) { - var body AnalyticsResponseOutputError0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromAnalyticsResponseOutputError0 overwrites any union data inside the AnalyticsResponseOutput_Error as the provided AnalyticsResponseOutputError0 -func (t *AnalyticsResponseOutput_Error) FromAnalyticsResponseOutputError0(v AnalyticsResponseOutputError0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeAnalyticsResponseOutputError0 performs a merge with any union data inside the AnalyticsResponseOutput_Error, using the provided AnalyticsResponseOutputError0 -func (t *AnalyticsResponseOutput_Error) MergeAnalyticsResponseOutputError0(v AnalyticsResponseOutputError0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsAnalyticsResponseOutputError1 returns the union data inside the AnalyticsResponseOutput_Error as a AnalyticsResponseOutputError1 -func (t AnalyticsResponseOutput_Error) AsAnalyticsResponseOutputError1() (AnalyticsResponseOutputError1, error) { - var body AnalyticsResponseOutputError1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromAnalyticsResponseOutputError1 overwrites any union data inside the AnalyticsResponseOutput_Error as the provided AnalyticsResponseOutputError1 -func (t *AnalyticsResponseOutput_Error) FromAnalyticsResponseOutputError1(v AnalyticsResponseOutputError1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeAnalyticsResponseOutputError1 performs a merge with any union data inside the AnalyticsResponseOutput_Error, using the provided AnalyticsResponseOutputError1 -func (t *AnalyticsResponseOutput_Error) MergeAnalyticsResponseOutputError1(v AnalyticsResponseOutputError1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t AnalyticsResponseOutput_Error) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *AnalyticsResponseOutput_Error) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsApplyProjectAddonBodyAddonVariant0 returns the union data inside the ApplyProjectAddonBody_AddonVariant as a ApplyProjectAddonBodyAddonVariant0 -func (t ApplyProjectAddonBody_AddonVariant) AsApplyProjectAddonBodyAddonVariant0() (ApplyProjectAddonBodyAddonVariant0, error) { - var body ApplyProjectAddonBodyAddonVariant0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromApplyProjectAddonBodyAddonVariant0 overwrites any union data inside the ApplyProjectAddonBody_AddonVariant as the provided ApplyProjectAddonBodyAddonVariant0 -func (t *ApplyProjectAddonBody_AddonVariant) FromApplyProjectAddonBodyAddonVariant0(v ApplyProjectAddonBodyAddonVariant0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeApplyProjectAddonBodyAddonVariant0 performs a merge with any union data inside the ApplyProjectAddonBody_AddonVariant, using the provided ApplyProjectAddonBodyAddonVariant0 -func (t *ApplyProjectAddonBody_AddonVariant) MergeApplyProjectAddonBodyAddonVariant0(v ApplyProjectAddonBodyAddonVariant0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsApplyProjectAddonBodyAddonVariant1 returns the union data inside the ApplyProjectAddonBody_AddonVariant as a ApplyProjectAddonBodyAddonVariant1 -func (t ApplyProjectAddonBody_AddonVariant) AsApplyProjectAddonBodyAddonVariant1() (ApplyProjectAddonBodyAddonVariant1, error) { - var body ApplyProjectAddonBodyAddonVariant1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromApplyProjectAddonBodyAddonVariant1 overwrites any union data inside the ApplyProjectAddonBody_AddonVariant as the provided ApplyProjectAddonBodyAddonVariant1 -func (t *ApplyProjectAddonBody_AddonVariant) FromApplyProjectAddonBodyAddonVariant1(v ApplyProjectAddonBodyAddonVariant1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeApplyProjectAddonBodyAddonVariant1 performs a merge with any union data inside the ApplyProjectAddonBody_AddonVariant, using the provided ApplyProjectAddonBodyAddonVariant1 -func (t *ApplyProjectAddonBody_AddonVariant) MergeApplyProjectAddonBodyAddonVariant1(v ApplyProjectAddonBodyAddonVariant1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsApplyProjectAddonBodyAddonVariant2 returns the union data inside the ApplyProjectAddonBody_AddonVariant as a ApplyProjectAddonBodyAddonVariant2 -func (t ApplyProjectAddonBody_AddonVariant) AsApplyProjectAddonBodyAddonVariant2() (ApplyProjectAddonBodyAddonVariant2, error) { - var body ApplyProjectAddonBodyAddonVariant2 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromApplyProjectAddonBodyAddonVariant2 overwrites any union data inside the ApplyProjectAddonBody_AddonVariant as the provided ApplyProjectAddonBodyAddonVariant2 -func (t *ApplyProjectAddonBody_AddonVariant) FromApplyProjectAddonBodyAddonVariant2(v ApplyProjectAddonBodyAddonVariant2) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeApplyProjectAddonBodyAddonVariant2 performs a merge with any union data inside the ApplyProjectAddonBody_AddonVariant, using the provided ApplyProjectAddonBodyAddonVariant2 -func (t *ApplyProjectAddonBody_AddonVariant) MergeApplyProjectAddonBodyAddonVariant2(v ApplyProjectAddonBodyAddonVariant2) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsApplyProjectAddonBodyAddonVariant3 returns the union data inside the ApplyProjectAddonBody_AddonVariant as a ApplyProjectAddonBodyAddonVariant3 -func (t ApplyProjectAddonBody_AddonVariant) AsApplyProjectAddonBodyAddonVariant3() (ApplyProjectAddonBodyAddonVariant3, error) { - var body ApplyProjectAddonBodyAddonVariant3 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromApplyProjectAddonBodyAddonVariant3 overwrites any union data inside the ApplyProjectAddonBody_AddonVariant as the provided ApplyProjectAddonBodyAddonVariant3 -func (t *ApplyProjectAddonBody_AddonVariant) FromApplyProjectAddonBodyAddonVariant3(v ApplyProjectAddonBodyAddonVariant3) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeApplyProjectAddonBodyAddonVariant3 performs a merge with any union data inside the ApplyProjectAddonBody_AddonVariant, using the provided ApplyProjectAddonBodyAddonVariant3 -func (t *ApplyProjectAddonBody_AddonVariant) MergeApplyProjectAddonBodyAddonVariant3(v ApplyProjectAddonBodyAddonVariant3) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t ApplyProjectAddonBody_AddonVariant) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *ApplyProjectAddonBody_AddonVariant) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsAuthorizeJitAccessBodyRhost0 returns the union data inside the AuthorizeJitAccessBody_Rhost as a AuthorizeJitAccessBodyRhost0 -func (t AuthorizeJitAccessBody_Rhost) AsAuthorizeJitAccessBodyRhost0() (AuthorizeJitAccessBodyRhost0, error) { - var body AuthorizeJitAccessBodyRhost0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromAuthorizeJitAccessBodyRhost0 overwrites any union data inside the AuthorizeJitAccessBody_Rhost as the provided AuthorizeJitAccessBodyRhost0 -func (t *AuthorizeJitAccessBody_Rhost) FromAuthorizeJitAccessBodyRhost0(v AuthorizeJitAccessBodyRhost0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeAuthorizeJitAccessBodyRhost0 performs a merge with any union data inside the AuthorizeJitAccessBody_Rhost, using the provided AuthorizeJitAccessBodyRhost0 -func (t *AuthorizeJitAccessBody_Rhost) MergeAuthorizeJitAccessBodyRhost0(v AuthorizeJitAccessBodyRhost0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsAuthorizeJitAccessBodyRhost1 returns the union data inside the AuthorizeJitAccessBody_Rhost as a AuthorizeJitAccessBodyRhost1 -func (t AuthorizeJitAccessBody_Rhost) AsAuthorizeJitAccessBodyRhost1() (AuthorizeJitAccessBodyRhost1, error) { - var body AuthorizeJitAccessBodyRhost1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromAuthorizeJitAccessBodyRhost1 overwrites any union data inside the AuthorizeJitAccessBody_Rhost as the provided AuthorizeJitAccessBodyRhost1 -func (t *AuthorizeJitAccessBody_Rhost) FromAuthorizeJitAccessBodyRhost1(v AuthorizeJitAccessBodyRhost1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeAuthorizeJitAccessBodyRhost1 performs a merge with any union data inside the AuthorizeJitAccessBody_Rhost, using the provided AuthorizeJitAccessBodyRhost1 -func (t *AuthorizeJitAccessBody_Rhost) MergeAuthorizeJitAccessBodyRhost1(v AuthorizeJitAccessBodyRhost1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t AuthorizeJitAccessBody_Rhost) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *AuthorizeJitAccessBody_Rhost) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsCreateSigningKeyBodyPrivateJwk0 returns the union data inside the CreateSigningKeyBody_PrivateJwk as a CreateSigningKeyBodyPrivateJwk0 -func (t CreateSigningKeyBody_PrivateJwk) AsCreateSigningKeyBodyPrivateJwk0() (CreateSigningKeyBodyPrivateJwk0, error) { - var body CreateSigningKeyBodyPrivateJwk0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromCreateSigningKeyBodyPrivateJwk0 overwrites any union data inside the CreateSigningKeyBody_PrivateJwk as the provided CreateSigningKeyBodyPrivateJwk0 -func (t *CreateSigningKeyBody_PrivateJwk) FromCreateSigningKeyBodyPrivateJwk0(v CreateSigningKeyBodyPrivateJwk0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeCreateSigningKeyBodyPrivateJwk0 performs a merge with any union data inside the CreateSigningKeyBody_PrivateJwk, using the provided CreateSigningKeyBodyPrivateJwk0 -func (t *CreateSigningKeyBody_PrivateJwk) MergeCreateSigningKeyBodyPrivateJwk0(v CreateSigningKeyBodyPrivateJwk0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsCreateSigningKeyBodyPrivateJwk1 returns the union data inside the CreateSigningKeyBody_PrivateJwk as a CreateSigningKeyBodyPrivateJwk1 -func (t CreateSigningKeyBody_PrivateJwk) AsCreateSigningKeyBodyPrivateJwk1() (CreateSigningKeyBodyPrivateJwk1, error) { - var body CreateSigningKeyBodyPrivateJwk1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromCreateSigningKeyBodyPrivateJwk1 overwrites any union data inside the CreateSigningKeyBody_PrivateJwk as the provided CreateSigningKeyBodyPrivateJwk1 -func (t *CreateSigningKeyBody_PrivateJwk) FromCreateSigningKeyBodyPrivateJwk1(v CreateSigningKeyBodyPrivateJwk1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeCreateSigningKeyBodyPrivateJwk1 performs a merge with any union data inside the CreateSigningKeyBody_PrivateJwk, using the provided CreateSigningKeyBodyPrivateJwk1 -func (t *CreateSigningKeyBody_PrivateJwk) MergeCreateSigningKeyBodyPrivateJwk1(v CreateSigningKeyBodyPrivateJwk1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsCreateSigningKeyBodyPrivateJwk2 returns the union data inside the CreateSigningKeyBody_PrivateJwk as a CreateSigningKeyBodyPrivateJwk2 -func (t CreateSigningKeyBody_PrivateJwk) AsCreateSigningKeyBodyPrivateJwk2() (CreateSigningKeyBodyPrivateJwk2, error) { - var body CreateSigningKeyBodyPrivateJwk2 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromCreateSigningKeyBodyPrivateJwk2 overwrites any union data inside the CreateSigningKeyBody_PrivateJwk as the provided CreateSigningKeyBodyPrivateJwk2 -func (t *CreateSigningKeyBody_PrivateJwk) FromCreateSigningKeyBodyPrivateJwk2(v CreateSigningKeyBodyPrivateJwk2) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeCreateSigningKeyBodyPrivateJwk2 performs a merge with any union data inside the CreateSigningKeyBody_PrivateJwk, using the provided CreateSigningKeyBodyPrivateJwk2 -func (t *CreateSigningKeyBody_PrivateJwk) MergeCreateSigningKeyBodyPrivateJwk2(v CreateSigningKeyBodyPrivateJwk2) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsCreateSigningKeyBodyPrivateJwk3 returns the union data inside the CreateSigningKeyBody_PrivateJwk as a CreateSigningKeyBodyPrivateJwk3 -func (t CreateSigningKeyBody_PrivateJwk) AsCreateSigningKeyBodyPrivateJwk3() (CreateSigningKeyBodyPrivateJwk3, error) { - var body CreateSigningKeyBodyPrivateJwk3 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromCreateSigningKeyBodyPrivateJwk3 overwrites any union data inside the CreateSigningKeyBody_PrivateJwk as the provided CreateSigningKeyBodyPrivateJwk3 -func (t *CreateSigningKeyBody_PrivateJwk) FromCreateSigningKeyBodyPrivateJwk3(v CreateSigningKeyBodyPrivateJwk3) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeCreateSigningKeyBodyPrivateJwk3 performs a merge with any union data inside the CreateSigningKeyBody_PrivateJwk, using the provided CreateSigningKeyBodyPrivateJwk3 -func (t *CreateSigningKeyBody_PrivateJwk) MergeCreateSigningKeyBodyPrivateJwk3(v CreateSigningKeyBodyPrivateJwk3) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t CreateSigningKeyBody_PrivateJwk) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *CreateSigningKeyBody_PrivateJwk) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsDiskRequestBodyAttributes0 returns the union data inside the DiskRequestBody_Attributes as a DiskRequestBodyAttributes0 -func (t DiskRequestBody_Attributes) AsDiskRequestBodyAttributes0() (DiskRequestBodyAttributes0, error) { - var body DiskRequestBodyAttributes0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromDiskRequestBodyAttributes0 overwrites any union data inside the DiskRequestBody_Attributes as the provided DiskRequestBodyAttributes0 -func (t *DiskRequestBody_Attributes) FromDiskRequestBodyAttributes0(v DiskRequestBodyAttributes0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeDiskRequestBodyAttributes0 performs a merge with any union data inside the DiskRequestBody_Attributes, using the provided DiskRequestBodyAttributes0 -func (t *DiskRequestBody_Attributes) MergeDiskRequestBodyAttributes0(v DiskRequestBodyAttributes0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsDiskRequestBodyAttributes1 returns the union data inside the DiskRequestBody_Attributes as a DiskRequestBodyAttributes1 -func (t DiskRequestBody_Attributes) AsDiskRequestBodyAttributes1() (DiskRequestBodyAttributes1, error) { - var body DiskRequestBodyAttributes1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromDiskRequestBodyAttributes1 overwrites any union data inside the DiskRequestBody_Attributes as the provided DiskRequestBodyAttributes1 -func (t *DiskRequestBody_Attributes) FromDiskRequestBodyAttributes1(v DiskRequestBodyAttributes1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeDiskRequestBodyAttributes1 performs a merge with any union data inside the DiskRequestBody_Attributes, using the provided DiskRequestBodyAttributes1 -func (t *DiskRequestBody_Attributes) MergeDiskRequestBodyAttributes1(v DiskRequestBodyAttributes1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t DiskRequestBody_Attributes) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *DiskRequestBody_Attributes) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsDiskResponseOutputAttributes0 returns the union data inside the DiskResponseOutput_Attributes as a DiskResponseOutputAttributes0 -func (t DiskResponseOutput_Attributes) AsDiskResponseOutputAttributes0() (DiskResponseOutputAttributes0, error) { - var body DiskResponseOutputAttributes0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromDiskResponseOutputAttributes0 overwrites any union data inside the DiskResponseOutput_Attributes as the provided DiskResponseOutputAttributes0 -func (t *DiskResponseOutput_Attributes) FromDiskResponseOutputAttributes0(v DiskResponseOutputAttributes0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeDiskResponseOutputAttributes0 performs a merge with any union data inside the DiskResponseOutput_Attributes, using the provided DiskResponseOutputAttributes0 -func (t *DiskResponseOutput_Attributes) MergeDiskResponseOutputAttributes0(v DiskResponseOutputAttributes0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsDiskResponseOutputAttributes1 returns the union data inside the DiskResponseOutput_Attributes as a DiskResponseOutputAttributes1 -func (t DiskResponseOutput_Attributes) AsDiskResponseOutputAttributes1() (DiskResponseOutputAttributes1, error) { - var body DiskResponseOutputAttributes1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromDiskResponseOutputAttributes1 overwrites any union data inside the DiskResponseOutput_Attributes as the provided DiskResponseOutputAttributes1 -func (t *DiskResponseOutput_Attributes) FromDiskResponseOutputAttributes1(v DiskResponseOutputAttributes1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeDiskResponseOutputAttributes1 performs a merge with any union data inside the DiskResponseOutput_Attributes, using the provided DiskResponseOutputAttributes1 -func (t *DiskResponseOutput_Attributes) MergeDiskResponseOutputAttributes1(v DiskResponseOutputAttributes1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t DiskResponseOutput_Attributes) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *DiskResponseOutput_Attributes) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsJitListAccessResponseOutputItems0 returns the union data inside the JitListAccessResponseOutput_Items_Item as a JitListAccessResponseOutputItems0 -func (t JitListAccessResponseOutput_Items_Item) AsJitListAccessResponseOutputItems0() (JitListAccessResponseOutputItems0, error) { - var body JitListAccessResponseOutputItems0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromJitListAccessResponseOutputItems0 overwrites any union data inside the JitListAccessResponseOutput_Items_Item as the provided JitListAccessResponseOutputItems0 -func (t *JitListAccessResponseOutput_Items_Item) FromJitListAccessResponseOutputItems0(v JitListAccessResponseOutputItems0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeJitListAccessResponseOutputItems0 performs a merge with any union data inside the JitListAccessResponseOutput_Items_Item, using the provided JitListAccessResponseOutputItems0 -func (t *JitListAccessResponseOutput_Items_Item) MergeJitListAccessResponseOutputItems0(v JitListAccessResponseOutputItems0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsJitListAccessResponseOutputItems1 returns the union data inside the JitListAccessResponseOutput_Items_Item as a JitListAccessResponseOutputItems1 -func (t JitListAccessResponseOutput_Items_Item) AsJitListAccessResponseOutputItems1() (JitListAccessResponseOutputItems1, error) { - var body JitListAccessResponseOutputItems1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromJitListAccessResponseOutputItems1 overwrites any union data inside the JitListAccessResponseOutput_Items_Item as the provided JitListAccessResponseOutputItems1 -func (t *JitListAccessResponseOutput_Items_Item) FromJitListAccessResponseOutputItems1(v JitListAccessResponseOutputItems1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeJitListAccessResponseOutputItems1 performs a merge with any union data inside the JitListAccessResponseOutput_Items_Item, using the provided JitListAccessResponseOutputItems1 -func (t *JitListAccessResponseOutput_Items_Item) MergeJitListAccessResponseOutputItems1(v JitListAccessResponseOutputItems1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t JitListAccessResponseOutput_Items_Item) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *JitListAccessResponseOutput_Items_Item) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsJsonValueOutput0 returns the union data inside the JsonValueOutput as a JsonValueOutput0 -func (t JsonValueOutput) AsJsonValueOutput0() (JsonValueOutput0, error) { - var body JsonValueOutput0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromJsonValueOutput0 overwrites any union data inside the JsonValueOutput as the provided JsonValueOutput0 -func (t *JsonValueOutput) FromJsonValueOutput0(v JsonValueOutput0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeJsonValueOutput0 performs a merge with any union data inside the JsonValueOutput, using the provided JsonValueOutput0 -func (t *JsonValueOutput) MergeJsonValueOutput0(v JsonValueOutput0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsJsonValueOutput1 returns the union data inside the JsonValueOutput as a JsonValueOutput1 -func (t JsonValueOutput) AsJsonValueOutput1() (JsonValueOutput1, error) { - var body JsonValueOutput1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromJsonValueOutput1 overwrites any union data inside the JsonValueOutput as the provided JsonValueOutput1 -func (t *JsonValueOutput) FromJsonValueOutput1(v JsonValueOutput1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeJsonValueOutput1 performs a merge with any union data inside the JsonValueOutput, using the provided JsonValueOutput1 -func (t *JsonValueOutput) MergeJsonValueOutput1(v JsonValueOutput1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsJsonValueOutput2 returns the union data inside the JsonValueOutput as a JsonValueOutput2 -func (t JsonValueOutput) AsJsonValueOutput2() (JsonValueOutput2, error) { - var body JsonValueOutput2 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromJsonValueOutput2 overwrites any union data inside the JsonValueOutput as the provided JsonValueOutput2 -func (t *JsonValueOutput) FromJsonValueOutput2(v JsonValueOutput2) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeJsonValueOutput2 performs a merge with any union data inside the JsonValueOutput, using the provided JsonValueOutput2 -func (t *JsonValueOutput) MergeJsonValueOutput2(v JsonValueOutput2) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t JsonValueOutput) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *JsonValueOutput) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsJsonValueOutput00 returns the union data inside the JsonValueOutput0 as a JsonValueOutput00 -func (t JsonValueOutput0) AsJsonValueOutput00() (JsonValueOutput00, error) { - var body JsonValueOutput00 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromJsonValueOutput00 overwrites any union data inside the JsonValueOutput0 as the provided JsonValueOutput00 -func (t *JsonValueOutput0) FromJsonValueOutput00(v JsonValueOutput00) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeJsonValueOutput00 performs a merge with any union data inside the JsonValueOutput0, using the provided JsonValueOutput00 -func (t *JsonValueOutput0) MergeJsonValueOutput00(v JsonValueOutput00) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsJsonValueOutput01 returns the union data inside the JsonValueOutput0 as a JsonValueOutput01 -func (t JsonValueOutput0) AsJsonValueOutput01() (JsonValueOutput01, error) { - var body JsonValueOutput01 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromJsonValueOutput01 overwrites any union data inside the JsonValueOutput0 as the provided JsonValueOutput01 -func (t *JsonValueOutput0) FromJsonValueOutput01(v JsonValueOutput01) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeJsonValueOutput01 performs a merge with any union data inside the JsonValueOutput0, using the provided JsonValueOutput01 -func (t *JsonValueOutput0) MergeJsonValueOutput01(v JsonValueOutput01) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsJsonValueOutput02 returns the union data inside the JsonValueOutput0 as a JsonValueOutput02 -func (t JsonValueOutput0) AsJsonValueOutput02() (JsonValueOutput02, error) { - var body JsonValueOutput02 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromJsonValueOutput02 overwrites any union data inside the JsonValueOutput0 as the provided JsonValueOutput02 -func (t *JsonValueOutput0) FromJsonValueOutput02(v JsonValueOutput02) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeJsonValueOutput02 performs a merge with any union data inside the JsonValueOutput0, using the provided JsonValueOutput02 -func (t *JsonValueOutput0) MergeJsonValueOutput02(v JsonValueOutput02) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t JsonValueOutput0) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *JsonValueOutput0) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsListProjectAddonsResponseOutputAvailableAddonsVariantsId0 returns the union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id as a ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 -func (t ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) AsListProjectAddonsResponseOutputAvailableAddonsVariantsId0() (ListProjectAddonsResponseOutputAvailableAddonsVariantsId0, error) { - var body ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromListProjectAddonsResponseOutputAvailableAddonsVariantsId0 overwrites any union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id as the provided ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 -func (t *ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) FromListProjectAddonsResponseOutputAvailableAddonsVariantsId0(v ListProjectAddonsResponseOutputAvailableAddonsVariantsId0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeListProjectAddonsResponseOutputAvailableAddonsVariantsId0 performs a merge with any union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id, using the provided ListProjectAddonsResponseOutputAvailableAddonsVariantsId0 -func (t *ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) MergeListProjectAddonsResponseOutputAvailableAddonsVariantsId0(v ListProjectAddonsResponseOutputAvailableAddonsVariantsId0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsListProjectAddonsResponseOutputAvailableAddonsVariantsId1 returns the union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id as a ListProjectAddonsResponseOutputAvailableAddonsVariantsId1 -func (t ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) AsListProjectAddonsResponseOutputAvailableAddonsVariantsId1() (ListProjectAddonsResponseOutputAvailableAddonsVariantsId1, error) { - var body ListProjectAddonsResponseOutputAvailableAddonsVariantsId1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromListProjectAddonsResponseOutputAvailableAddonsVariantsId1 overwrites any union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id as the provided ListProjectAddonsResponseOutputAvailableAddonsVariantsId1 -func (t *ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) FromListProjectAddonsResponseOutputAvailableAddonsVariantsId1(v ListProjectAddonsResponseOutputAvailableAddonsVariantsId1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeListProjectAddonsResponseOutputAvailableAddonsVariantsId1 performs a merge with any union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id, using the provided ListProjectAddonsResponseOutputAvailableAddonsVariantsId1 -func (t *ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) MergeListProjectAddonsResponseOutputAvailableAddonsVariantsId1(v ListProjectAddonsResponseOutputAvailableAddonsVariantsId1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsListProjectAddonsResponseOutputAvailableAddonsVariantsId2 returns the union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id as a ListProjectAddonsResponseOutputAvailableAddonsVariantsId2 -func (t ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) AsListProjectAddonsResponseOutputAvailableAddonsVariantsId2() (ListProjectAddonsResponseOutputAvailableAddonsVariantsId2, error) { - var body ListProjectAddonsResponseOutputAvailableAddonsVariantsId2 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromListProjectAddonsResponseOutputAvailableAddonsVariantsId2 overwrites any union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id as the provided ListProjectAddonsResponseOutputAvailableAddonsVariantsId2 -func (t *ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) FromListProjectAddonsResponseOutputAvailableAddonsVariantsId2(v ListProjectAddonsResponseOutputAvailableAddonsVariantsId2) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeListProjectAddonsResponseOutputAvailableAddonsVariantsId2 performs a merge with any union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id, using the provided ListProjectAddonsResponseOutputAvailableAddonsVariantsId2 -func (t *ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) MergeListProjectAddonsResponseOutputAvailableAddonsVariantsId2(v ListProjectAddonsResponseOutputAvailableAddonsVariantsId2) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsListProjectAddonsResponseOutputAvailableAddonsVariantsId3 returns the union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id as a ListProjectAddonsResponseOutputAvailableAddonsVariantsId3 -func (t ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) AsListProjectAddonsResponseOutputAvailableAddonsVariantsId3() (ListProjectAddonsResponseOutputAvailableAddonsVariantsId3, error) { - var body ListProjectAddonsResponseOutputAvailableAddonsVariantsId3 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromListProjectAddonsResponseOutputAvailableAddonsVariantsId3 overwrites any union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id as the provided ListProjectAddonsResponseOutputAvailableAddonsVariantsId3 -func (t *ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) FromListProjectAddonsResponseOutputAvailableAddonsVariantsId3(v ListProjectAddonsResponseOutputAvailableAddonsVariantsId3) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeListProjectAddonsResponseOutputAvailableAddonsVariantsId3 performs a merge with any union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id, using the provided ListProjectAddonsResponseOutputAvailableAddonsVariantsId3 -func (t *ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) MergeListProjectAddonsResponseOutputAvailableAddonsVariantsId3(v ListProjectAddonsResponseOutputAvailableAddonsVariantsId3) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsListProjectAddonsResponseOutputAvailableAddonsVariantsId4 returns the union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id as a ListProjectAddonsResponseOutputAvailableAddonsVariantsId4 -func (t ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) AsListProjectAddonsResponseOutputAvailableAddonsVariantsId4() (ListProjectAddonsResponseOutputAvailableAddonsVariantsId4, error) { - var body ListProjectAddonsResponseOutputAvailableAddonsVariantsId4 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromListProjectAddonsResponseOutputAvailableAddonsVariantsId4 overwrites any union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id as the provided ListProjectAddonsResponseOutputAvailableAddonsVariantsId4 -func (t *ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) FromListProjectAddonsResponseOutputAvailableAddonsVariantsId4(v ListProjectAddonsResponseOutputAvailableAddonsVariantsId4) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeListProjectAddonsResponseOutputAvailableAddonsVariantsId4 performs a merge with any union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id, using the provided ListProjectAddonsResponseOutputAvailableAddonsVariantsId4 -func (t *ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) MergeListProjectAddonsResponseOutputAvailableAddonsVariantsId4(v ListProjectAddonsResponseOutputAvailableAddonsVariantsId4) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsListProjectAddonsResponseOutputAvailableAddonsVariantsId5 returns the union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id as a ListProjectAddonsResponseOutputAvailableAddonsVariantsId5 -func (t ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) AsListProjectAddonsResponseOutputAvailableAddonsVariantsId5() (ListProjectAddonsResponseOutputAvailableAddonsVariantsId5, error) { - var body ListProjectAddonsResponseOutputAvailableAddonsVariantsId5 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromListProjectAddonsResponseOutputAvailableAddonsVariantsId5 overwrites any union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id as the provided ListProjectAddonsResponseOutputAvailableAddonsVariantsId5 -func (t *ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) FromListProjectAddonsResponseOutputAvailableAddonsVariantsId5(v ListProjectAddonsResponseOutputAvailableAddonsVariantsId5) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeListProjectAddonsResponseOutputAvailableAddonsVariantsId5 performs a merge with any union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id, using the provided ListProjectAddonsResponseOutputAvailableAddonsVariantsId5 -func (t *ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) MergeListProjectAddonsResponseOutputAvailableAddonsVariantsId5(v ListProjectAddonsResponseOutputAvailableAddonsVariantsId5) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsListProjectAddonsResponseOutputAvailableAddonsVariantsId6 returns the union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id as a ListProjectAddonsResponseOutputAvailableAddonsVariantsId6 -func (t ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) AsListProjectAddonsResponseOutputAvailableAddonsVariantsId6() (ListProjectAddonsResponseOutputAvailableAddonsVariantsId6, error) { - var body ListProjectAddonsResponseOutputAvailableAddonsVariantsId6 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromListProjectAddonsResponseOutputAvailableAddonsVariantsId6 overwrites any union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id as the provided ListProjectAddonsResponseOutputAvailableAddonsVariantsId6 -func (t *ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) FromListProjectAddonsResponseOutputAvailableAddonsVariantsId6(v ListProjectAddonsResponseOutputAvailableAddonsVariantsId6) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeListProjectAddonsResponseOutputAvailableAddonsVariantsId6 performs a merge with any union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id, using the provided ListProjectAddonsResponseOutputAvailableAddonsVariantsId6 -func (t *ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) MergeListProjectAddonsResponseOutputAvailableAddonsVariantsId6(v ListProjectAddonsResponseOutputAvailableAddonsVariantsId6) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsListProjectAddonsResponseOutputAvailableAddonsVariantsId7 returns the union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id as a ListProjectAddonsResponseOutputAvailableAddonsVariantsId7 -func (t ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) AsListProjectAddonsResponseOutputAvailableAddonsVariantsId7() (ListProjectAddonsResponseOutputAvailableAddonsVariantsId7, error) { - var body ListProjectAddonsResponseOutputAvailableAddonsVariantsId7 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromListProjectAddonsResponseOutputAvailableAddonsVariantsId7 overwrites any union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id as the provided ListProjectAddonsResponseOutputAvailableAddonsVariantsId7 -func (t *ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) FromListProjectAddonsResponseOutputAvailableAddonsVariantsId7(v ListProjectAddonsResponseOutputAvailableAddonsVariantsId7) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeListProjectAddonsResponseOutputAvailableAddonsVariantsId7 performs a merge with any union data inside the ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id, using the provided ListProjectAddonsResponseOutputAvailableAddonsVariantsId7 -func (t *ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) MergeListProjectAddonsResponseOutputAvailableAddonsVariantsId7(v ListProjectAddonsResponseOutputAvailableAddonsVariantsId7) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *ListProjectAddonsResponseOutput_AvailableAddons_Variants_Id) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsListProjectAddonsResponseOutputSelectedAddonsVariantId0 returns the union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id as a ListProjectAddonsResponseOutputSelectedAddonsVariantId0 -func (t ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) AsListProjectAddonsResponseOutputSelectedAddonsVariantId0() (ListProjectAddonsResponseOutputSelectedAddonsVariantId0, error) { - var body ListProjectAddonsResponseOutputSelectedAddonsVariantId0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromListProjectAddonsResponseOutputSelectedAddonsVariantId0 overwrites any union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id as the provided ListProjectAddonsResponseOutputSelectedAddonsVariantId0 -func (t *ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) FromListProjectAddonsResponseOutputSelectedAddonsVariantId0(v ListProjectAddonsResponseOutputSelectedAddonsVariantId0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeListProjectAddonsResponseOutputSelectedAddonsVariantId0 performs a merge with any union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id, using the provided ListProjectAddonsResponseOutputSelectedAddonsVariantId0 -func (t *ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) MergeListProjectAddonsResponseOutputSelectedAddonsVariantId0(v ListProjectAddonsResponseOutputSelectedAddonsVariantId0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsListProjectAddonsResponseOutputSelectedAddonsVariantId1 returns the union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id as a ListProjectAddonsResponseOutputSelectedAddonsVariantId1 -func (t ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) AsListProjectAddonsResponseOutputSelectedAddonsVariantId1() (ListProjectAddonsResponseOutputSelectedAddonsVariantId1, error) { - var body ListProjectAddonsResponseOutputSelectedAddonsVariantId1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromListProjectAddonsResponseOutputSelectedAddonsVariantId1 overwrites any union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id as the provided ListProjectAddonsResponseOutputSelectedAddonsVariantId1 -func (t *ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) FromListProjectAddonsResponseOutputSelectedAddonsVariantId1(v ListProjectAddonsResponseOutputSelectedAddonsVariantId1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeListProjectAddonsResponseOutputSelectedAddonsVariantId1 performs a merge with any union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id, using the provided ListProjectAddonsResponseOutputSelectedAddonsVariantId1 -func (t *ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) MergeListProjectAddonsResponseOutputSelectedAddonsVariantId1(v ListProjectAddonsResponseOutputSelectedAddonsVariantId1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsListProjectAddonsResponseOutputSelectedAddonsVariantId2 returns the union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id as a ListProjectAddonsResponseOutputSelectedAddonsVariantId2 -func (t ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) AsListProjectAddonsResponseOutputSelectedAddonsVariantId2() (ListProjectAddonsResponseOutputSelectedAddonsVariantId2, error) { - var body ListProjectAddonsResponseOutputSelectedAddonsVariantId2 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromListProjectAddonsResponseOutputSelectedAddonsVariantId2 overwrites any union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id as the provided ListProjectAddonsResponseOutputSelectedAddonsVariantId2 -func (t *ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) FromListProjectAddonsResponseOutputSelectedAddonsVariantId2(v ListProjectAddonsResponseOutputSelectedAddonsVariantId2) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeListProjectAddonsResponseOutputSelectedAddonsVariantId2 performs a merge with any union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id, using the provided ListProjectAddonsResponseOutputSelectedAddonsVariantId2 -func (t *ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) MergeListProjectAddonsResponseOutputSelectedAddonsVariantId2(v ListProjectAddonsResponseOutputSelectedAddonsVariantId2) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsListProjectAddonsResponseOutputSelectedAddonsVariantId3 returns the union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id as a ListProjectAddonsResponseOutputSelectedAddonsVariantId3 -func (t ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) AsListProjectAddonsResponseOutputSelectedAddonsVariantId3() (ListProjectAddonsResponseOutputSelectedAddonsVariantId3, error) { - var body ListProjectAddonsResponseOutputSelectedAddonsVariantId3 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromListProjectAddonsResponseOutputSelectedAddonsVariantId3 overwrites any union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id as the provided ListProjectAddonsResponseOutputSelectedAddonsVariantId3 -func (t *ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) FromListProjectAddonsResponseOutputSelectedAddonsVariantId3(v ListProjectAddonsResponseOutputSelectedAddonsVariantId3) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeListProjectAddonsResponseOutputSelectedAddonsVariantId3 performs a merge with any union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id, using the provided ListProjectAddonsResponseOutputSelectedAddonsVariantId3 -func (t *ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) MergeListProjectAddonsResponseOutputSelectedAddonsVariantId3(v ListProjectAddonsResponseOutputSelectedAddonsVariantId3) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsListProjectAddonsResponseOutputSelectedAddonsVariantId4 returns the union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id as a ListProjectAddonsResponseOutputSelectedAddonsVariantId4 -func (t ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) AsListProjectAddonsResponseOutputSelectedAddonsVariantId4() (ListProjectAddonsResponseOutputSelectedAddonsVariantId4, error) { - var body ListProjectAddonsResponseOutputSelectedAddonsVariantId4 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromListProjectAddonsResponseOutputSelectedAddonsVariantId4 overwrites any union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id as the provided ListProjectAddonsResponseOutputSelectedAddonsVariantId4 -func (t *ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) FromListProjectAddonsResponseOutputSelectedAddonsVariantId4(v ListProjectAddonsResponseOutputSelectedAddonsVariantId4) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeListProjectAddonsResponseOutputSelectedAddonsVariantId4 performs a merge with any union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id, using the provided ListProjectAddonsResponseOutputSelectedAddonsVariantId4 -func (t *ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) MergeListProjectAddonsResponseOutputSelectedAddonsVariantId4(v ListProjectAddonsResponseOutputSelectedAddonsVariantId4) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsListProjectAddonsResponseOutputSelectedAddonsVariantId5 returns the union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id as a ListProjectAddonsResponseOutputSelectedAddonsVariantId5 -func (t ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) AsListProjectAddonsResponseOutputSelectedAddonsVariantId5() (ListProjectAddonsResponseOutputSelectedAddonsVariantId5, error) { - var body ListProjectAddonsResponseOutputSelectedAddonsVariantId5 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromListProjectAddonsResponseOutputSelectedAddonsVariantId5 overwrites any union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id as the provided ListProjectAddonsResponseOutputSelectedAddonsVariantId5 -func (t *ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) FromListProjectAddonsResponseOutputSelectedAddonsVariantId5(v ListProjectAddonsResponseOutputSelectedAddonsVariantId5) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeListProjectAddonsResponseOutputSelectedAddonsVariantId5 performs a merge with any union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id, using the provided ListProjectAddonsResponseOutputSelectedAddonsVariantId5 -func (t *ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) MergeListProjectAddonsResponseOutputSelectedAddonsVariantId5(v ListProjectAddonsResponseOutputSelectedAddonsVariantId5) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsListProjectAddonsResponseOutputSelectedAddonsVariantId6 returns the union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id as a ListProjectAddonsResponseOutputSelectedAddonsVariantId6 -func (t ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) AsListProjectAddonsResponseOutputSelectedAddonsVariantId6() (ListProjectAddonsResponseOutputSelectedAddonsVariantId6, error) { - var body ListProjectAddonsResponseOutputSelectedAddonsVariantId6 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromListProjectAddonsResponseOutputSelectedAddonsVariantId6 overwrites any union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id as the provided ListProjectAddonsResponseOutputSelectedAddonsVariantId6 -func (t *ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) FromListProjectAddonsResponseOutputSelectedAddonsVariantId6(v ListProjectAddonsResponseOutputSelectedAddonsVariantId6) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeListProjectAddonsResponseOutputSelectedAddonsVariantId6 performs a merge with any union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id, using the provided ListProjectAddonsResponseOutputSelectedAddonsVariantId6 -func (t *ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) MergeListProjectAddonsResponseOutputSelectedAddonsVariantId6(v ListProjectAddonsResponseOutputSelectedAddonsVariantId6) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsListProjectAddonsResponseOutputSelectedAddonsVariantId7 returns the union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id as a ListProjectAddonsResponseOutputSelectedAddonsVariantId7 -func (t ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) AsListProjectAddonsResponseOutputSelectedAddonsVariantId7() (ListProjectAddonsResponseOutputSelectedAddonsVariantId7, error) { - var body ListProjectAddonsResponseOutputSelectedAddonsVariantId7 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromListProjectAddonsResponseOutputSelectedAddonsVariantId7 overwrites any union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id as the provided ListProjectAddonsResponseOutputSelectedAddonsVariantId7 -func (t *ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) FromListProjectAddonsResponseOutputSelectedAddonsVariantId7(v ListProjectAddonsResponseOutputSelectedAddonsVariantId7) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeListProjectAddonsResponseOutputSelectedAddonsVariantId7 performs a merge with any union data inside the ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id, using the provided ListProjectAddonsResponseOutputSelectedAddonsVariantId7 -func (t *ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) MergeListProjectAddonsResponseOutputSelectedAddonsVariantId7(v ListProjectAddonsResponseOutputSelectedAddonsVariantId7) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *ListProjectAddonsResponseOutput_SelectedAddons_Variant_Id) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType0 returns the union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_6_ObjType as a ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType0 -func (t ProjectUpgradeEligibilityResponseOutput_ValidationErrors_6_ObjType) AsProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType0() (ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType0, error) { - var body ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType0 overwrites any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_6_ObjType as the provided ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType0 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_6_ObjType) FromProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType0(v ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType0 performs a merge with any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_6_ObjType, using the provided ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType0 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_6_ObjType) MergeProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType0(v ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType1 returns the union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_6_ObjType as a ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType1 -func (t ProjectUpgradeEligibilityResponseOutput_ValidationErrors_6_ObjType) AsProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType1() (ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType1, error) { - var body ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType1 overwrites any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_6_ObjType as the provided ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType1 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_6_ObjType) FromProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType1(v ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType1 performs a merge with any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_6_ObjType, using the provided ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType1 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_6_ObjType) MergeProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType1(v ProjectUpgradeEligibilityResponseOutputValidationErrors6ObjType1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t ProjectUpgradeEligibilityResponseOutput_ValidationErrors_6_ObjType) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_6_ObjType) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsProjectUpgradeEligibilityResponseOutputValidationErrors0 returns the union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item as a ProjectUpgradeEligibilityResponseOutputValidationErrors0 -func (t ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) AsProjectUpgradeEligibilityResponseOutputValidationErrors0() (ProjectUpgradeEligibilityResponseOutputValidationErrors0, error) { - var body ProjectUpgradeEligibilityResponseOutputValidationErrors0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromProjectUpgradeEligibilityResponseOutputValidationErrors0 overwrites any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item as the provided ProjectUpgradeEligibilityResponseOutputValidationErrors0 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) FromProjectUpgradeEligibilityResponseOutputValidationErrors0(v ProjectUpgradeEligibilityResponseOutputValidationErrors0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeProjectUpgradeEligibilityResponseOutputValidationErrors0 performs a merge with any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item, using the provided ProjectUpgradeEligibilityResponseOutputValidationErrors0 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) MergeProjectUpgradeEligibilityResponseOutputValidationErrors0(v ProjectUpgradeEligibilityResponseOutputValidationErrors0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsProjectUpgradeEligibilityResponseOutputValidationErrors1 returns the union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item as a ProjectUpgradeEligibilityResponseOutputValidationErrors1 -func (t ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) AsProjectUpgradeEligibilityResponseOutputValidationErrors1() (ProjectUpgradeEligibilityResponseOutputValidationErrors1, error) { - var body ProjectUpgradeEligibilityResponseOutputValidationErrors1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromProjectUpgradeEligibilityResponseOutputValidationErrors1 overwrites any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item as the provided ProjectUpgradeEligibilityResponseOutputValidationErrors1 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) FromProjectUpgradeEligibilityResponseOutputValidationErrors1(v ProjectUpgradeEligibilityResponseOutputValidationErrors1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeProjectUpgradeEligibilityResponseOutputValidationErrors1 performs a merge with any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item, using the provided ProjectUpgradeEligibilityResponseOutputValidationErrors1 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) MergeProjectUpgradeEligibilityResponseOutputValidationErrors1(v ProjectUpgradeEligibilityResponseOutputValidationErrors1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsProjectUpgradeEligibilityResponseOutputValidationErrors2 returns the union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item as a ProjectUpgradeEligibilityResponseOutputValidationErrors2 -func (t ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) AsProjectUpgradeEligibilityResponseOutputValidationErrors2() (ProjectUpgradeEligibilityResponseOutputValidationErrors2, error) { - var body ProjectUpgradeEligibilityResponseOutputValidationErrors2 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromProjectUpgradeEligibilityResponseOutputValidationErrors2 overwrites any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item as the provided ProjectUpgradeEligibilityResponseOutputValidationErrors2 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) FromProjectUpgradeEligibilityResponseOutputValidationErrors2(v ProjectUpgradeEligibilityResponseOutputValidationErrors2) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeProjectUpgradeEligibilityResponseOutputValidationErrors2 performs a merge with any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item, using the provided ProjectUpgradeEligibilityResponseOutputValidationErrors2 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) MergeProjectUpgradeEligibilityResponseOutputValidationErrors2(v ProjectUpgradeEligibilityResponseOutputValidationErrors2) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsProjectUpgradeEligibilityResponseOutputValidationErrors3 returns the union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item as a ProjectUpgradeEligibilityResponseOutputValidationErrors3 -func (t ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) AsProjectUpgradeEligibilityResponseOutputValidationErrors3() (ProjectUpgradeEligibilityResponseOutputValidationErrors3, error) { - var body ProjectUpgradeEligibilityResponseOutputValidationErrors3 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromProjectUpgradeEligibilityResponseOutputValidationErrors3 overwrites any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item as the provided ProjectUpgradeEligibilityResponseOutputValidationErrors3 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) FromProjectUpgradeEligibilityResponseOutputValidationErrors3(v ProjectUpgradeEligibilityResponseOutputValidationErrors3) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeProjectUpgradeEligibilityResponseOutputValidationErrors3 performs a merge with any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item, using the provided ProjectUpgradeEligibilityResponseOutputValidationErrors3 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) MergeProjectUpgradeEligibilityResponseOutputValidationErrors3(v ProjectUpgradeEligibilityResponseOutputValidationErrors3) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsProjectUpgradeEligibilityResponseOutputValidationErrors4 returns the union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item as a ProjectUpgradeEligibilityResponseOutputValidationErrors4 -func (t ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) AsProjectUpgradeEligibilityResponseOutputValidationErrors4() (ProjectUpgradeEligibilityResponseOutputValidationErrors4, error) { - var body ProjectUpgradeEligibilityResponseOutputValidationErrors4 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromProjectUpgradeEligibilityResponseOutputValidationErrors4 overwrites any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item as the provided ProjectUpgradeEligibilityResponseOutputValidationErrors4 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) FromProjectUpgradeEligibilityResponseOutputValidationErrors4(v ProjectUpgradeEligibilityResponseOutputValidationErrors4) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeProjectUpgradeEligibilityResponseOutputValidationErrors4 performs a merge with any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item, using the provided ProjectUpgradeEligibilityResponseOutputValidationErrors4 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) MergeProjectUpgradeEligibilityResponseOutputValidationErrors4(v ProjectUpgradeEligibilityResponseOutputValidationErrors4) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsProjectUpgradeEligibilityResponseOutputValidationErrors5 returns the union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item as a ProjectUpgradeEligibilityResponseOutputValidationErrors5 -func (t ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) AsProjectUpgradeEligibilityResponseOutputValidationErrors5() (ProjectUpgradeEligibilityResponseOutputValidationErrors5, error) { - var body ProjectUpgradeEligibilityResponseOutputValidationErrors5 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromProjectUpgradeEligibilityResponseOutputValidationErrors5 overwrites any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item as the provided ProjectUpgradeEligibilityResponseOutputValidationErrors5 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) FromProjectUpgradeEligibilityResponseOutputValidationErrors5(v ProjectUpgradeEligibilityResponseOutputValidationErrors5) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeProjectUpgradeEligibilityResponseOutputValidationErrors5 performs a merge with any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item, using the provided ProjectUpgradeEligibilityResponseOutputValidationErrors5 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) MergeProjectUpgradeEligibilityResponseOutputValidationErrors5(v ProjectUpgradeEligibilityResponseOutputValidationErrors5) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsProjectUpgradeEligibilityResponseOutputValidationErrors6 returns the union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item as a ProjectUpgradeEligibilityResponseOutputValidationErrors6 -func (t ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) AsProjectUpgradeEligibilityResponseOutputValidationErrors6() (ProjectUpgradeEligibilityResponseOutputValidationErrors6, error) { - var body ProjectUpgradeEligibilityResponseOutputValidationErrors6 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromProjectUpgradeEligibilityResponseOutputValidationErrors6 overwrites any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item as the provided ProjectUpgradeEligibilityResponseOutputValidationErrors6 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) FromProjectUpgradeEligibilityResponseOutputValidationErrors6(v ProjectUpgradeEligibilityResponseOutputValidationErrors6) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeProjectUpgradeEligibilityResponseOutputValidationErrors6 performs a merge with any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item, using the provided ProjectUpgradeEligibilityResponseOutputValidationErrors6 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) MergeProjectUpgradeEligibilityResponseOutputValidationErrors6(v ProjectUpgradeEligibilityResponseOutputValidationErrors6) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsProjectUpgradeEligibilityResponseOutputValidationErrors7 returns the union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item as a ProjectUpgradeEligibilityResponseOutputValidationErrors7 -func (t ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) AsProjectUpgradeEligibilityResponseOutputValidationErrors7() (ProjectUpgradeEligibilityResponseOutputValidationErrors7, error) { - var body ProjectUpgradeEligibilityResponseOutputValidationErrors7 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromProjectUpgradeEligibilityResponseOutputValidationErrors7 overwrites any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item as the provided ProjectUpgradeEligibilityResponseOutputValidationErrors7 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) FromProjectUpgradeEligibilityResponseOutputValidationErrors7(v ProjectUpgradeEligibilityResponseOutputValidationErrors7) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeProjectUpgradeEligibilityResponseOutputValidationErrors7 performs a merge with any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item, using the provided ProjectUpgradeEligibilityResponseOutputValidationErrors7 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) MergeProjectUpgradeEligibilityResponseOutputValidationErrors7(v ProjectUpgradeEligibilityResponseOutputValidationErrors7) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsProjectUpgradeEligibilityResponseOutputValidationErrors8 returns the union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item as a ProjectUpgradeEligibilityResponseOutputValidationErrors8 -func (t ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) AsProjectUpgradeEligibilityResponseOutputValidationErrors8() (ProjectUpgradeEligibilityResponseOutputValidationErrors8, error) { - var body ProjectUpgradeEligibilityResponseOutputValidationErrors8 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromProjectUpgradeEligibilityResponseOutputValidationErrors8 overwrites any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item as the provided ProjectUpgradeEligibilityResponseOutputValidationErrors8 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) FromProjectUpgradeEligibilityResponseOutputValidationErrors8(v ProjectUpgradeEligibilityResponseOutputValidationErrors8) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeProjectUpgradeEligibilityResponseOutputValidationErrors8 performs a merge with any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item, using the provided ProjectUpgradeEligibilityResponseOutputValidationErrors8 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) MergeProjectUpgradeEligibilityResponseOutputValidationErrors8(v ProjectUpgradeEligibilityResponseOutputValidationErrors8) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsProjectUpgradeEligibilityResponseOutputValidationErrors9 returns the union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item as a ProjectUpgradeEligibilityResponseOutputValidationErrors9 -func (t ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) AsProjectUpgradeEligibilityResponseOutputValidationErrors9() (ProjectUpgradeEligibilityResponseOutputValidationErrors9, error) { - var body ProjectUpgradeEligibilityResponseOutputValidationErrors9 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromProjectUpgradeEligibilityResponseOutputValidationErrors9 overwrites any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item as the provided ProjectUpgradeEligibilityResponseOutputValidationErrors9 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) FromProjectUpgradeEligibilityResponseOutputValidationErrors9(v ProjectUpgradeEligibilityResponseOutputValidationErrors9) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeProjectUpgradeEligibilityResponseOutputValidationErrors9 performs a merge with any union data inside the ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item, using the provided ProjectUpgradeEligibilityResponseOutputValidationErrors9 -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) MergeProjectUpgradeEligibilityResponseOutputValidationErrors9(v ProjectUpgradeEligibilityResponseOutputValidationErrors9) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *ProjectUpgradeEligibilityResponseOutput_ValidationErrors_Item) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsProjectUpgradeEligibilityResponseOutputWarnings0 returns the union data inside the ProjectUpgradeEligibilityResponseOutput_Warnings_Item as a ProjectUpgradeEligibilityResponseOutputWarnings0 -func (t ProjectUpgradeEligibilityResponseOutput_Warnings_Item) AsProjectUpgradeEligibilityResponseOutputWarnings0() (ProjectUpgradeEligibilityResponseOutputWarnings0, error) { - var body ProjectUpgradeEligibilityResponseOutputWarnings0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromProjectUpgradeEligibilityResponseOutputWarnings0 overwrites any union data inside the ProjectUpgradeEligibilityResponseOutput_Warnings_Item as the provided ProjectUpgradeEligibilityResponseOutputWarnings0 -func (t *ProjectUpgradeEligibilityResponseOutput_Warnings_Item) FromProjectUpgradeEligibilityResponseOutputWarnings0(v ProjectUpgradeEligibilityResponseOutputWarnings0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeProjectUpgradeEligibilityResponseOutputWarnings0 performs a merge with any union data inside the ProjectUpgradeEligibilityResponseOutput_Warnings_Item, using the provided ProjectUpgradeEligibilityResponseOutputWarnings0 -func (t *ProjectUpgradeEligibilityResponseOutput_Warnings_Item) MergeProjectUpgradeEligibilityResponseOutputWarnings0(v ProjectUpgradeEligibilityResponseOutputWarnings0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsProjectUpgradeEligibilityResponseOutputWarnings1 returns the union data inside the ProjectUpgradeEligibilityResponseOutput_Warnings_Item as a ProjectUpgradeEligibilityResponseOutputWarnings1 -func (t ProjectUpgradeEligibilityResponseOutput_Warnings_Item) AsProjectUpgradeEligibilityResponseOutputWarnings1() (ProjectUpgradeEligibilityResponseOutputWarnings1, error) { - var body ProjectUpgradeEligibilityResponseOutputWarnings1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromProjectUpgradeEligibilityResponseOutputWarnings1 overwrites any union data inside the ProjectUpgradeEligibilityResponseOutput_Warnings_Item as the provided ProjectUpgradeEligibilityResponseOutputWarnings1 -func (t *ProjectUpgradeEligibilityResponseOutput_Warnings_Item) FromProjectUpgradeEligibilityResponseOutputWarnings1(v ProjectUpgradeEligibilityResponseOutputWarnings1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeProjectUpgradeEligibilityResponseOutputWarnings1 performs a merge with any union data inside the ProjectUpgradeEligibilityResponseOutput_Warnings_Item, using the provided ProjectUpgradeEligibilityResponseOutputWarnings1 -func (t *ProjectUpgradeEligibilityResponseOutput_Warnings_Item) MergeProjectUpgradeEligibilityResponseOutputWarnings1(v ProjectUpgradeEligibilityResponseOutputWarnings1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsProjectUpgradeEligibilityResponseOutputWarnings2 returns the union data inside the ProjectUpgradeEligibilityResponseOutput_Warnings_Item as a ProjectUpgradeEligibilityResponseOutputWarnings2 -func (t ProjectUpgradeEligibilityResponseOutput_Warnings_Item) AsProjectUpgradeEligibilityResponseOutputWarnings2() (ProjectUpgradeEligibilityResponseOutputWarnings2, error) { - var body ProjectUpgradeEligibilityResponseOutputWarnings2 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromProjectUpgradeEligibilityResponseOutputWarnings2 overwrites any union data inside the ProjectUpgradeEligibilityResponseOutput_Warnings_Item as the provided ProjectUpgradeEligibilityResponseOutputWarnings2 -func (t *ProjectUpgradeEligibilityResponseOutput_Warnings_Item) FromProjectUpgradeEligibilityResponseOutputWarnings2(v ProjectUpgradeEligibilityResponseOutputWarnings2) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeProjectUpgradeEligibilityResponseOutputWarnings2 performs a merge with any union data inside the ProjectUpgradeEligibilityResponseOutput_Warnings_Item, using the provided ProjectUpgradeEligibilityResponseOutputWarnings2 -func (t *ProjectUpgradeEligibilityResponseOutput_Warnings_Item) MergeProjectUpgradeEligibilityResponseOutputWarnings2(v ProjectUpgradeEligibilityResponseOutputWarnings2) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsProjectUpgradeEligibilityResponseOutputWarnings3 returns the union data inside the ProjectUpgradeEligibilityResponseOutput_Warnings_Item as a ProjectUpgradeEligibilityResponseOutputWarnings3 -func (t ProjectUpgradeEligibilityResponseOutput_Warnings_Item) AsProjectUpgradeEligibilityResponseOutputWarnings3() (ProjectUpgradeEligibilityResponseOutputWarnings3, error) { - var body ProjectUpgradeEligibilityResponseOutputWarnings3 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromProjectUpgradeEligibilityResponseOutputWarnings3 overwrites any union data inside the ProjectUpgradeEligibilityResponseOutput_Warnings_Item as the provided ProjectUpgradeEligibilityResponseOutputWarnings3 -func (t *ProjectUpgradeEligibilityResponseOutput_Warnings_Item) FromProjectUpgradeEligibilityResponseOutputWarnings3(v ProjectUpgradeEligibilityResponseOutputWarnings3) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeProjectUpgradeEligibilityResponseOutputWarnings3 performs a merge with any union data inside the ProjectUpgradeEligibilityResponseOutput_Warnings_Item, using the provided ProjectUpgradeEligibilityResponseOutputWarnings3 -func (t *ProjectUpgradeEligibilityResponseOutput_Warnings_Item) MergeProjectUpgradeEligibilityResponseOutputWarnings3(v ProjectUpgradeEligibilityResponseOutputWarnings3) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t ProjectUpgradeEligibilityResponseOutput_Warnings_Item) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *ProjectUpgradeEligibilityResponseOutput_Warnings_Item) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsV1CreateProjectBodyRegionSelection0 returns the union data inside the V1CreateProjectBody_RegionSelection as a V1CreateProjectBodyRegionSelection0 -func (t V1CreateProjectBody_RegionSelection) AsV1CreateProjectBodyRegionSelection0() (V1CreateProjectBodyRegionSelection0, error) { - var body V1CreateProjectBodyRegionSelection0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromV1CreateProjectBodyRegionSelection0 overwrites any union data inside the V1CreateProjectBody_RegionSelection as the provided V1CreateProjectBodyRegionSelection0 -func (t *V1CreateProjectBody_RegionSelection) FromV1CreateProjectBodyRegionSelection0(v V1CreateProjectBodyRegionSelection0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeV1CreateProjectBodyRegionSelection0 performs a merge with any union data inside the V1CreateProjectBody_RegionSelection, using the provided V1CreateProjectBodyRegionSelection0 -func (t *V1CreateProjectBody_RegionSelection) MergeV1CreateProjectBodyRegionSelection0(v V1CreateProjectBodyRegionSelection0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsV1CreateProjectBodyRegionSelection1 returns the union data inside the V1CreateProjectBody_RegionSelection as a V1CreateProjectBodyRegionSelection1 -func (t V1CreateProjectBody_RegionSelection) AsV1CreateProjectBodyRegionSelection1() (V1CreateProjectBodyRegionSelection1, error) { - var body V1CreateProjectBodyRegionSelection1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromV1CreateProjectBodyRegionSelection1 overwrites any union data inside the V1CreateProjectBody_RegionSelection as the provided V1CreateProjectBodyRegionSelection1 -func (t *V1CreateProjectBody_RegionSelection) FromV1CreateProjectBodyRegionSelection1(v V1CreateProjectBodyRegionSelection1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeV1CreateProjectBodyRegionSelection1 performs a merge with any union data inside the V1CreateProjectBody_RegionSelection, using the provided V1CreateProjectBodyRegionSelection1 -func (t *V1CreateProjectBody_RegionSelection) MergeV1CreateProjectBodyRegionSelection1(v V1CreateProjectBodyRegionSelection1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t V1CreateProjectBody_RegionSelection) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *V1CreateProjectBody_RegionSelection) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsV1GetUsageApiCountResponseOutputError0 returns the union data inside the V1GetUsageApiCountResponseOutput_Error as a V1GetUsageApiCountResponseOutputError0 -func (t V1GetUsageApiCountResponseOutput_Error) AsV1GetUsageApiCountResponseOutputError0() (V1GetUsageApiCountResponseOutputError0, error) { - var body V1GetUsageApiCountResponseOutputError0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromV1GetUsageApiCountResponseOutputError0 overwrites any union data inside the V1GetUsageApiCountResponseOutput_Error as the provided V1GetUsageApiCountResponseOutputError0 -func (t *V1GetUsageApiCountResponseOutput_Error) FromV1GetUsageApiCountResponseOutputError0(v V1GetUsageApiCountResponseOutputError0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeV1GetUsageApiCountResponseOutputError0 performs a merge with any union data inside the V1GetUsageApiCountResponseOutput_Error, using the provided V1GetUsageApiCountResponseOutputError0 -func (t *V1GetUsageApiCountResponseOutput_Error) MergeV1GetUsageApiCountResponseOutputError0(v V1GetUsageApiCountResponseOutputError0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsV1GetUsageApiCountResponseOutputError1 returns the union data inside the V1GetUsageApiCountResponseOutput_Error as a V1GetUsageApiCountResponseOutputError1 -func (t V1GetUsageApiCountResponseOutput_Error) AsV1GetUsageApiCountResponseOutputError1() (V1GetUsageApiCountResponseOutputError1, error) { - var body V1GetUsageApiCountResponseOutputError1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromV1GetUsageApiCountResponseOutputError1 overwrites any union data inside the V1GetUsageApiCountResponseOutput_Error as the provided V1GetUsageApiCountResponseOutputError1 -func (t *V1GetUsageApiCountResponseOutput_Error) FromV1GetUsageApiCountResponseOutputError1(v V1GetUsageApiCountResponseOutputError1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeV1GetUsageApiCountResponseOutputError1 performs a merge with any union data inside the V1GetUsageApiCountResponseOutput_Error, using the provided V1GetUsageApiCountResponseOutputError1 -func (t *V1GetUsageApiCountResponseOutput_Error) MergeV1GetUsageApiCountResponseOutputError1(v V1GetUsageApiCountResponseOutputError1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t V1GetUsageApiCountResponseOutput_Error) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *V1GetUsageApiCountResponseOutput_Error) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsV1GetUsageApiRequestsCountResponseOutputError0 returns the union data inside the V1GetUsageApiRequestsCountResponseOutput_Error as a V1GetUsageApiRequestsCountResponseOutputError0 -func (t V1GetUsageApiRequestsCountResponseOutput_Error) AsV1GetUsageApiRequestsCountResponseOutputError0() (V1GetUsageApiRequestsCountResponseOutputError0, error) { - var body V1GetUsageApiRequestsCountResponseOutputError0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromV1GetUsageApiRequestsCountResponseOutputError0 overwrites any union data inside the V1GetUsageApiRequestsCountResponseOutput_Error as the provided V1GetUsageApiRequestsCountResponseOutputError0 -func (t *V1GetUsageApiRequestsCountResponseOutput_Error) FromV1GetUsageApiRequestsCountResponseOutputError0(v V1GetUsageApiRequestsCountResponseOutputError0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeV1GetUsageApiRequestsCountResponseOutputError0 performs a merge with any union data inside the V1GetUsageApiRequestsCountResponseOutput_Error, using the provided V1GetUsageApiRequestsCountResponseOutputError0 -func (t *V1GetUsageApiRequestsCountResponseOutput_Error) MergeV1GetUsageApiRequestsCountResponseOutputError0(v V1GetUsageApiRequestsCountResponseOutputError0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsV1GetUsageApiRequestsCountResponseOutputError1 returns the union data inside the V1GetUsageApiRequestsCountResponseOutput_Error as a V1GetUsageApiRequestsCountResponseOutputError1 -func (t V1GetUsageApiRequestsCountResponseOutput_Error) AsV1GetUsageApiRequestsCountResponseOutputError1() (V1GetUsageApiRequestsCountResponseOutputError1, error) { - var body V1GetUsageApiRequestsCountResponseOutputError1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromV1GetUsageApiRequestsCountResponseOutputError1 overwrites any union data inside the V1GetUsageApiRequestsCountResponseOutput_Error as the provided V1GetUsageApiRequestsCountResponseOutputError1 -func (t *V1GetUsageApiRequestsCountResponseOutput_Error) FromV1GetUsageApiRequestsCountResponseOutputError1(v V1GetUsageApiRequestsCountResponseOutputError1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeV1GetUsageApiRequestsCountResponseOutputError1 performs a merge with any union data inside the V1GetUsageApiRequestsCountResponseOutput_Error, using the provided V1GetUsageApiRequestsCountResponseOutputError1 -func (t *V1GetUsageApiRequestsCountResponseOutput_Error) MergeV1GetUsageApiRequestsCountResponseOutputError1(v V1GetUsageApiRequestsCountResponseOutputError1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t V1GetUsageApiRequestsCountResponseOutput_Error) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *V1GetUsageApiRequestsCountResponseOutput_Error) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsV1ListEntitlementsResponseOutputEntitlementsConfig0 returns the union data inside the V1ListEntitlementsResponseOutput_Entitlements_Config as a V1ListEntitlementsResponseOutputEntitlementsConfig0 -func (t V1ListEntitlementsResponseOutput_Entitlements_Config) AsV1ListEntitlementsResponseOutputEntitlementsConfig0() (V1ListEntitlementsResponseOutputEntitlementsConfig0, error) { - var body V1ListEntitlementsResponseOutputEntitlementsConfig0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromV1ListEntitlementsResponseOutputEntitlementsConfig0 overwrites any union data inside the V1ListEntitlementsResponseOutput_Entitlements_Config as the provided V1ListEntitlementsResponseOutputEntitlementsConfig0 -func (t *V1ListEntitlementsResponseOutput_Entitlements_Config) FromV1ListEntitlementsResponseOutputEntitlementsConfig0(v V1ListEntitlementsResponseOutputEntitlementsConfig0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeV1ListEntitlementsResponseOutputEntitlementsConfig0 performs a merge with any union data inside the V1ListEntitlementsResponseOutput_Entitlements_Config, using the provided V1ListEntitlementsResponseOutputEntitlementsConfig0 -func (t *V1ListEntitlementsResponseOutput_Entitlements_Config) MergeV1ListEntitlementsResponseOutputEntitlementsConfig0(v V1ListEntitlementsResponseOutputEntitlementsConfig0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsV1ListEntitlementsResponseOutputEntitlementsConfig1 returns the union data inside the V1ListEntitlementsResponseOutput_Entitlements_Config as a V1ListEntitlementsResponseOutputEntitlementsConfig1 -func (t V1ListEntitlementsResponseOutput_Entitlements_Config) AsV1ListEntitlementsResponseOutputEntitlementsConfig1() (V1ListEntitlementsResponseOutputEntitlementsConfig1, error) { - var body V1ListEntitlementsResponseOutputEntitlementsConfig1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromV1ListEntitlementsResponseOutputEntitlementsConfig1 overwrites any union data inside the V1ListEntitlementsResponseOutput_Entitlements_Config as the provided V1ListEntitlementsResponseOutputEntitlementsConfig1 -func (t *V1ListEntitlementsResponseOutput_Entitlements_Config) FromV1ListEntitlementsResponseOutputEntitlementsConfig1(v V1ListEntitlementsResponseOutputEntitlementsConfig1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeV1ListEntitlementsResponseOutputEntitlementsConfig1 performs a merge with any union data inside the V1ListEntitlementsResponseOutput_Entitlements_Config, using the provided V1ListEntitlementsResponseOutputEntitlementsConfig1 -func (t *V1ListEntitlementsResponseOutput_Entitlements_Config) MergeV1ListEntitlementsResponseOutputEntitlementsConfig1(v V1ListEntitlementsResponseOutputEntitlementsConfig1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsV1ListEntitlementsResponseOutputEntitlementsConfig2 returns the union data inside the V1ListEntitlementsResponseOutput_Entitlements_Config as a V1ListEntitlementsResponseOutputEntitlementsConfig2 -func (t V1ListEntitlementsResponseOutput_Entitlements_Config) AsV1ListEntitlementsResponseOutputEntitlementsConfig2() (V1ListEntitlementsResponseOutputEntitlementsConfig2, error) { - var body V1ListEntitlementsResponseOutputEntitlementsConfig2 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromV1ListEntitlementsResponseOutputEntitlementsConfig2 overwrites any union data inside the V1ListEntitlementsResponseOutput_Entitlements_Config as the provided V1ListEntitlementsResponseOutputEntitlementsConfig2 -func (t *V1ListEntitlementsResponseOutput_Entitlements_Config) FromV1ListEntitlementsResponseOutputEntitlementsConfig2(v V1ListEntitlementsResponseOutputEntitlementsConfig2) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeV1ListEntitlementsResponseOutputEntitlementsConfig2 performs a merge with any union data inside the V1ListEntitlementsResponseOutput_Entitlements_Config, using the provided V1ListEntitlementsResponseOutputEntitlementsConfig2 -func (t *V1ListEntitlementsResponseOutput_Entitlements_Config) MergeV1ListEntitlementsResponseOutputEntitlementsConfig2(v V1ListEntitlementsResponseOutputEntitlementsConfig2) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t V1ListEntitlementsResponseOutput_Entitlements_Config) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *V1ListEntitlementsResponseOutput_Entitlements_Config) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsV1ServiceHealthResponseOutputInfo0 returns the union data inside the V1ServiceHealthResponseOutput_Info as a V1ServiceHealthResponseOutputInfo0 -func (t V1ServiceHealthResponseOutput_Info) AsV1ServiceHealthResponseOutputInfo0() (V1ServiceHealthResponseOutputInfo0, error) { - var body V1ServiceHealthResponseOutputInfo0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromV1ServiceHealthResponseOutputInfo0 overwrites any union data inside the V1ServiceHealthResponseOutput_Info as the provided V1ServiceHealthResponseOutputInfo0 -func (t *V1ServiceHealthResponseOutput_Info) FromV1ServiceHealthResponseOutputInfo0(v V1ServiceHealthResponseOutputInfo0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeV1ServiceHealthResponseOutputInfo0 performs a merge with any union data inside the V1ServiceHealthResponseOutput_Info, using the provided V1ServiceHealthResponseOutputInfo0 -func (t *V1ServiceHealthResponseOutput_Info) MergeV1ServiceHealthResponseOutputInfo0(v V1ServiceHealthResponseOutputInfo0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsV1ServiceHealthResponseOutputInfo1 returns the union data inside the V1ServiceHealthResponseOutput_Info as a V1ServiceHealthResponseOutputInfo1 -func (t V1ServiceHealthResponseOutput_Info) AsV1ServiceHealthResponseOutputInfo1() (V1ServiceHealthResponseOutputInfo1, error) { - var body V1ServiceHealthResponseOutputInfo1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromV1ServiceHealthResponseOutputInfo1 overwrites any union data inside the V1ServiceHealthResponseOutput_Info as the provided V1ServiceHealthResponseOutputInfo1 -func (t *V1ServiceHealthResponseOutput_Info) FromV1ServiceHealthResponseOutputInfo1(v V1ServiceHealthResponseOutputInfo1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeV1ServiceHealthResponseOutputInfo1 performs a merge with any union data inside the V1ServiceHealthResponseOutput_Info, using the provided V1ServiceHealthResponseOutputInfo1 -func (t *V1ServiceHealthResponseOutput_Info) MergeV1ServiceHealthResponseOutputInfo1(v V1ServiceHealthResponseOutputInfo1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsV1ServiceHealthResponseOutputInfo2 returns the union data inside the V1ServiceHealthResponseOutput_Info as a V1ServiceHealthResponseOutputInfo2 -func (t V1ServiceHealthResponseOutput_Info) AsV1ServiceHealthResponseOutputInfo2() (V1ServiceHealthResponseOutputInfo2, error) { - var body V1ServiceHealthResponseOutputInfo2 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromV1ServiceHealthResponseOutputInfo2 overwrites any union data inside the V1ServiceHealthResponseOutput_Info as the provided V1ServiceHealthResponseOutputInfo2 -func (t *V1ServiceHealthResponseOutput_Info) FromV1ServiceHealthResponseOutputInfo2(v V1ServiceHealthResponseOutputInfo2) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeV1ServiceHealthResponseOutputInfo2 performs a merge with any union data inside the V1ServiceHealthResponseOutput_Info, using the provided V1ServiceHealthResponseOutputInfo2 -func (t *V1ServiceHealthResponseOutput_Info) MergeV1ServiceHealthResponseOutputInfo2(v V1ServiceHealthResponseOutputInfo2) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t V1ServiceHealthResponseOutput_Info) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *V1ServiceHealthResponseOutput_Info) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsV1GetJitAccessConfig200JSONResponseBody0 returns the union data inside the V1GetJitAccessConfig200JSONResponseBody as a V1GetJitAccessConfig200JSONResponseBody0 -func (t V1GetJitAccessConfig200JSONResponseBody) AsV1GetJitAccessConfig200JSONResponseBody0() (V1GetJitAccessConfig200JSONResponseBody0, error) { - var body V1GetJitAccessConfig200JSONResponseBody0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromV1GetJitAccessConfig200JSONResponseBody0 overwrites any union data inside the V1GetJitAccessConfig200JSONResponseBody as the provided V1GetJitAccessConfig200JSONResponseBody0 -func (t *V1GetJitAccessConfig200JSONResponseBody) FromV1GetJitAccessConfig200JSONResponseBody0(v V1GetJitAccessConfig200JSONResponseBody0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeV1GetJitAccessConfig200JSONResponseBody0 performs a merge with any union data inside the V1GetJitAccessConfig200JSONResponseBody, using the provided V1GetJitAccessConfig200JSONResponseBody0 -func (t *V1GetJitAccessConfig200JSONResponseBody) MergeV1GetJitAccessConfig200JSONResponseBody0(v V1GetJitAccessConfig200JSONResponseBody0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsV1GetJitAccessConfig200JSONResponseBody1 returns the union data inside the V1GetJitAccessConfig200JSONResponseBody as a V1GetJitAccessConfig200JSONResponseBody1 -func (t V1GetJitAccessConfig200JSONResponseBody) AsV1GetJitAccessConfig200JSONResponseBody1() (V1GetJitAccessConfig200JSONResponseBody1, error) { - var body V1GetJitAccessConfig200JSONResponseBody1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromV1GetJitAccessConfig200JSONResponseBody1 overwrites any union data inside the V1GetJitAccessConfig200JSONResponseBody as the provided V1GetJitAccessConfig200JSONResponseBody1 -func (t *V1GetJitAccessConfig200JSONResponseBody) FromV1GetJitAccessConfig200JSONResponseBody1(v V1GetJitAccessConfig200JSONResponseBody1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeV1GetJitAccessConfig200JSONResponseBody1 performs a merge with any union data inside the V1GetJitAccessConfig200JSONResponseBody, using the provided V1GetJitAccessConfig200JSONResponseBody1 -func (t *V1GetJitAccessConfig200JSONResponseBody) MergeV1GetJitAccessConfig200JSONResponseBody1(v V1GetJitAccessConfig200JSONResponseBody1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t V1GetJitAccessConfig200JSONResponseBody) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *V1GetJitAccessConfig200JSONResponseBody) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsV1UpdateJitAccessConfig200JSONResponseBody0 returns the union data inside the V1UpdateJitAccessConfig200JSONResponseBody as a V1UpdateJitAccessConfig200JSONResponseBody0 -func (t V1UpdateJitAccessConfig200JSONResponseBody) AsV1UpdateJitAccessConfig200JSONResponseBody0() (V1UpdateJitAccessConfig200JSONResponseBody0, error) { - var body V1UpdateJitAccessConfig200JSONResponseBody0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromV1UpdateJitAccessConfig200JSONResponseBody0 overwrites any union data inside the V1UpdateJitAccessConfig200JSONResponseBody as the provided V1UpdateJitAccessConfig200JSONResponseBody0 -func (t *V1UpdateJitAccessConfig200JSONResponseBody) FromV1UpdateJitAccessConfig200JSONResponseBody0(v V1UpdateJitAccessConfig200JSONResponseBody0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeV1UpdateJitAccessConfig200JSONResponseBody0 performs a merge with any union data inside the V1UpdateJitAccessConfig200JSONResponseBody, using the provided V1UpdateJitAccessConfig200JSONResponseBody0 -func (t *V1UpdateJitAccessConfig200JSONResponseBody) MergeV1UpdateJitAccessConfig200JSONResponseBody0(v V1UpdateJitAccessConfig200JSONResponseBody0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsV1UpdateJitAccessConfig200JSONResponseBody1 returns the union data inside the V1UpdateJitAccessConfig200JSONResponseBody as a V1UpdateJitAccessConfig200JSONResponseBody1 -func (t V1UpdateJitAccessConfig200JSONResponseBody) AsV1UpdateJitAccessConfig200JSONResponseBody1() (V1UpdateJitAccessConfig200JSONResponseBody1, error) { - var body V1UpdateJitAccessConfig200JSONResponseBody1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromV1UpdateJitAccessConfig200JSONResponseBody1 overwrites any union data inside the V1UpdateJitAccessConfig200JSONResponseBody as the provided V1UpdateJitAccessConfig200JSONResponseBody1 -func (t *V1UpdateJitAccessConfig200JSONResponseBody) FromV1UpdateJitAccessConfig200JSONResponseBody1(v V1UpdateJitAccessConfig200JSONResponseBody1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeV1UpdateJitAccessConfig200JSONResponseBody1 performs a merge with any union data inside the V1UpdateJitAccessConfig200JSONResponseBody, using the provided V1UpdateJitAccessConfig200JSONResponseBody1 -func (t *V1UpdateJitAccessConfig200JSONResponseBody) MergeV1UpdateJitAccessConfig200JSONResponseBody1(v V1UpdateJitAccessConfig200JSONResponseBody1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t V1UpdateJitAccessConfig200JSONResponseBody) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *V1UpdateJitAccessConfig200JSONResponseBody) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} diff --git a/apps/cli-go/pkg/cast/cast.go b/apps/cli-go/pkg/cast/cast.go deleted file mode 100644 index c4038e1815..0000000000 --- a/apps/cli-go/pkg/cast/cast.go +++ /dev/null @@ -1,62 +0,0 @@ -package cast - -import ( - "math" - "strconv" -) - -// UintToInt converts a uint to an int, handling potential overflow -func UintToInt(value uint) int { - if value <= math.MaxInt { - return int(value) - } - return math.MaxInt -} - -// UIntToUInt16 converts a uint to an uint16, handling potential overflow -func UIntToUInt16(value uint) uint16 { - if value <= math.MaxUint16 { - return uint16(value) - } - return math.MaxUint16 -} - -// IntToUint converts an int to a uint, handling negative values -func IntToUint(value int) uint { - if value < 0 { - return 0 - } - return uint(value) -} - -func UintToIntPtr(value *uint) *int { - if value == nil { - return nil - } - return Ptr(UintToInt(*value)) -} - -func IntToUintPtr(value *int) *uint { - if value == nil { - return nil - } - return Ptr(IntToUint(*value)) -} - -func BoolToStringPtr(value *bool) *string { - if value == nil { - return nil - } - return Ptr(strconv.FormatBool(*value)) -} - -func Ptr[T any](v T) *T { - return &v -} - -func Val[T any](v *T, def T) T { - if v == nil { - return def - } - return *v -} diff --git a/apps/cli-go/pkg/config/api.go b/apps/cli-go/pkg/config/api.go deleted file mode 100644 index 2c97565b7b..0000000000 --- a/apps/cli-go/pkg/config/api.go +++ /dev/null @@ -1,106 +0,0 @@ -package config - -import ( - "strings" - - v1API "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/cast" - "github.com/supabase/cli/pkg/diff" -) - -type ( - api struct { - Enabled bool `toml:"enabled" json:"enabled"` - Schemas []string `toml:"schemas" json:"schemas"` - ExtraSearchPath []string `toml:"extra_search_path" json:"extra_search_path"` - MaxRows uint `toml:"max_rows" json:"max_rows"` - // When unset (default), new tables, views, sequences and functions created in the - // `public` schema by `postgres` are NOT automatically reachable through the Data API - // roles `anon`, `authenticated`, and `service_role`; explicit GRANTs are required, - // matching the new cloud default. Set to true to keep the legacy behaviour of - // auto-exposing new entities (deprecated; the field is removed on 2026-10-30). Set to - // false to pin the new default explicitly. Stored as a pointer so the migration path - // (unset -> default true before May 30 2026, default false after, removed in October) - // can flip the implicit value without losing the explicit user choice. - AutoExposeNewTables *bool `toml:"auto_expose_new_tables,omitempty" json:"auto_expose_new_tables,omitempty"` - // Local only config - Image string `toml:"-" json:"-"` - KongImage string `toml:"-" json:"-"` - Port uint16 `toml:"port" json:"port"` - Tls tlsKong `toml:"tls" json:"tls"` - // TODO: replace [auth|studio].api_url - ExternalUrl string `toml:"external_url" json:"external_url"` - } - - tlsKong struct { - Enabled bool `toml:"enabled" json:"enabled"` - CertPath string `toml:"cert_path" json:"cert_path"` - CertContent []byte `toml:"-" json:"-"` - KeyPath string `toml:"key_path" json:"key_path"` - KeyContent []byte `toml:"-" json:"-"` - } -) - -func (a *api) ToUpdatePostgrestConfigBody() v1API.V1UpdatePostgrestConfigBody { - body := v1API.V1UpdatePostgrestConfigBody{} - - // When the api is disabled, remote side it just set the dbSchema to an empty value - if !a.Enabled { - body.DbSchema = cast.Ptr("") - return body - } - - // Convert Schemas to a comma-separated string - if len(a.Schemas) > 0 { - schemas := strings.Join(a.Schemas, ",") - body.DbSchema = &schemas - } - - // Convert ExtraSearchPath to a comma-separated string - body.DbExtraSearchPath = cast.Ptr(strings.Join(a.ExtraSearchPath, ",")) - - // Convert MaxRows to int pointer - if a.MaxRows > 0 { - body.MaxRows = cast.Ptr(cast.UintToInt(a.MaxRows)) - } - - // Note: DbPool is not present in the Api struct, so it's not set here - return body -} - -func (a *api) FromRemoteApiConfig(remoteConfig v1API.PostgrestConfigWithJWTSecretResponseOutput) { - if a.Enabled = len(remoteConfig.DbSchema) > 0; !a.Enabled { - return - } - - // Update Schemas if present in remoteConfig - a.Schemas = strToArr(remoteConfig.DbSchema) - // TODO: use slices.Map when upgrade go version - for i, schema := range a.Schemas { - a.Schemas[i] = strings.TrimSpace(schema) - } - - // Update ExtraSearchPath if present in remoteConfig - a.ExtraSearchPath = strToArr(remoteConfig.DbExtraSearchPath) - for i, path := range a.ExtraSearchPath { - a.ExtraSearchPath[i] = strings.TrimSpace(path) - } - - // Update MaxRows if present in remoteConfig - a.MaxRows = cast.IntToUint(remoteConfig.MaxRows) -} - -func (a *api) DiffWithRemote(remoteConfig v1API.PostgrestConfigWithJWTSecretResponseOutput) ([]byte, error) { - copy := *a - // Convert the config values into easily comparable remoteConfig values - currentValue, err := ToTomlBytes(copy) - if err != nil { - return nil, err - } - copy.FromRemoteApiConfig(remoteConfig) - remoteCompare, err := ToTomlBytes(copy) - if err != nil { - return nil, err - } - return diff.Diff("remote[api]", remoteCompare, "local[api]", currentValue), nil -} diff --git a/apps/cli-go/pkg/config/api_test.go b/apps/cli-go/pkg/config/api_test.go deleted file mode 100644 index 8ce75c5365..0000000000 --- a/apps/cli-go/pkg/config/api_test.go +++ /dev/null @@ -1,197 +0,0 @@ -package config - -import ( - "bytes" - "io" - "os" - "testing" - fs "testing/fstest" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - v1API "github.com/supabase/cli/pkg/api" -) - -func TestApiToUpdatePostgrestConfigBody(t *testing.T) { - t.Run("converts all fields correctly", func(t *testing.T) { - api := &api{ - Enabled: true, - Schemas: []string{"public", "private"}, - ExtraSearchPath: []string{"extensions", "public"}, - MaxRows: 1000, - } - - body := api.ToUpdatePostgrestConfigBody() - - assert.Equal(t, "public,private", *body.DbSchema) - assert.Equal(t, "extensions,public", *body.DbExtraSearchPath) - assert.Equal(t, 1000, *body.MaxRows) - }) - - t.Run("handles empty fields", func(t *testing.T) { - api := &api{} - - body := api.ToUpdatePostgrestConfigBody() - - // remote api will be false by default, leading to an empty schema on api side - assert.Equal(t, "", *body.DbSchema) - }) -} - -func TestApiDiff(t *testing.T) { - t.Run("detects differences", func(t *testing.T) { - api := &api{ - Enabled: true, - Schemas: []string{"public", "private"}, - ExtraSearchPath: []string{"extensions", "public"}, - MaxRows: 1000, - } - - remoteConfig := v1API.PostgrestConfigWithJWTSecretResponseOutput{ - DbSchema: "public", - DbExtraSearchPath: "public", - MaxRows: 500, - } - - diff, err := api.DiffWithRemote(remoteConfig) - assert.NoError(t, err) - - assertSnapshotEqual(t, diff) - }) - - t.Run("handles no differences", func(t *testing.T) { - api := &api{ - Enabled: true, - Schemas: []string{"public"}, - ExtraSearchPath: []string{"public"}, - MaxRows: 500, - } - - remoteConfig := v1API.PostgrestConfigWithJWTSecretResponseOutput{ - DbSchema: "public", - DbExtraSearchPath: "public", - MaxRows: 500, - } - - diff, err := api.DiffWithRemote(remoteConfig) - assert.NoError(t, err) - - assert.Empty(t, diff) - }) - - t.Run("handles multiple schemas and search paths with spaces", func(t *testing.T) { - api := &api{ - Enabled: true, - Schemas: []string{"public", "private"}, - ExtraSearchPath: []string{"extensions", "public"}, - MaxRows: 500, - } - - remoteConfig := v1API.PostgrestConfigWithJWTSecretResponseOutput{ - DbSchema: "public, private", - DbExtraSearchPath: "extensions, public", - MaxRows: 500, - } - - diff, err := api.DiffWithRemote(remoteConfig) - assert.NoError(t, err) - - assert.Empty(t, diff) - }) - - t.Run("handles api disabled on remote side", func(t *testing.T) { - api := &api{ - Enabled: true, - Schemas: []string{"public", "private"}, - ExtraSearchPath: []string{"extensions", "public"}, - MaxRows: 500, - } - - remoteConfig := v1API.PostgrestConfigWithJWTSecretResponseOutput{ - DbSchema: "", - DbExtraSearchPath: "", - MaxRows: 0, - } - - diff, err := api.DiffWithRemote(remoteConfig) - assert.NoError(t, err) - - assertSnapshotEqual(t, diff) - }) - - t.Run("handles api disabled on local side", func(t *testing.T) { - api := &api{ - Enabled: false, - Schemas: []string{"public"}, - ExtraSearchPath: []string{"public"}, - MaxRows: 500, - } - - remoteConfig := v1API.PostgrestConfigWithJWTSecretResponseOutput{ - DbSchema: "public", - DbExtraSearchPath: "public", - MaxRows: 500, - } - - diff, err := api.DiffWithRemote(remoteConfig) - assert.NoError(t, err) - - assertSnapshotEqual(t, diff) - }) -} - -func TestApiAutoExposeNewTablesDefault(t *testing.T) { - t.Run("is unset on a fresh config so the implicit revoke-by-default behaviour applies", func(t *testing.T) { - cfg := NewConfig() - assert.Nil(t, cfg.Api.AutoExposeNewTables) - }) -} - -func TestApiAutoExposeNewTablesWarning(t *testing.T) { - captureStderr := func(t *testing.T, run func()) string { - t.Helper() - r, w, err := os.Pipe() - require.NoError(t, err) - orig := os.Stderr - os.Stderr = w - defer func() { os.Stderr = orig }() - run() - require.NoError(t, w.Close()) - var out bytes.Buffer - _, err = io.Copy(&out, r) - require.NoError(t, err) - return out.String() - } - - t.Run("warns when auto_expose_new_tables is explicitly true", func(t *testing.T) { - cfg := NewConfig() - fsys := fs.MapFS{"config.toml": &fs.MapFile{Data: []byte("[api]\nauto_expose_new_tables = true\n")}} - stderr := captureStderr(t, func() { - require.NoError(t, cfg.Load("config.toml", fsys)) - }) - require.NotNil(t, cfg.Api.AutoExposeNewTables) - assert.True(t, *cfg.Api.AutoExposeNewTables) - assert.Contains(t, stderr, "api.auto_expose_new_tables is deprecated") - assert.Contains(t, stderr, "2026-10-30") - }) - - t.Run("does not warn when unset", func(t *testing.T) { - cfg := NewConfig() - stderr := captureStderr(t, func() { - require.NoError(t, cfg.Load("", fs.MapFS{})) - }) - assert.Nil(t, cfg.Api.AutoExposeNewTables) - assert.NotContains(t, stderr, "auto_expose_new_tables is deprecated") - }) - - t.Run("does not warn when explicitly false", func(t *testing.T) { - cfg := NewConfig() - fsys := fs.MapFS{"config.toml": &fs.MapFile{Data: []byte("[api]\nauto_expose_new_tables = false\n")}} - stderr := captureStderr(t, func() { - require.NoError(t, cfg.Load("config.toml", fsys)) - }) - require.NotNil(t, cfg.Api.AutoExposeNewTables) - assert.False(t, *cfg.Api.AutoExposeNewTables) - assert.NotContains(t, stderr, "auto_expose_new_tables is deprecated") - }) -} diff --git a/apps/cli-go/pkg/config/apikeys.go b/apps/cli-go/pkg/config/apikeys.go deleted file mode 100644 index cb1b457cab..0000000000 --- a/apps/cli-go/pkg/config/apikeys.go +++ /dev/null @@ -1,211 +0,0 @@ -package config - -import ( - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rsa" - "encoding/base64" - "math/big" - "time" - - "github.com/go-errors/errors" - "github.com/golang-jwt/jwt/v5" -) - -const ( - defaultJwtSecret = "super-secret-jwt-token-with-at-least-32-characters-long" - defaultJwtExpiry = 1983812996 - defaultPublishableKey = "sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH" - defaultSecretKey = "sb_secret_N7UND0UgjKTVK-Uodkm0Hg_xSvEMPvz" -) - -type CustomClaims struct { - // Overrides Issuer to maintain json order when marshalling - Issuer string `json:"iss,omitempty"` - Ref string `json:"ref,omitempty"` - Role string `json:"role"` - IsAnon bool `json:"is_anonymous,omitempty"` - jwt.RegisteredClaims -} - -func (c CustomClaims) NewToken() *jwt.Token { - if c.ExpiresAt == nil { - c.ExpiresAt = jwt.NewNumericDate(time.Unix(defaultJwtExpiry, 0)) - } - if len(c.Issuer) == 0 { - c.Issuer = "supabase-demo" - } - return jwt.NewWithClaims(jwt.SigningMethodHS256, c) -} - -// generateAPIKeys generates JWT tokens using the appropriate signing method -func (a *auth) generateAPIKeys() error { - if len(a.JwtSecret.Value) == 0 { - a.JwtSecret.Value = defaultJwtSecret - } else if len(a.JwtSecret.Value) < 16 { - return errors.Errorf("Invalid config for auth.jwt_secret. Must be at least 16 characters") - } - // Generate anon key if not provided - if len(a.AnonKey.Value) == 0 { - signed, err := a.generateJWT("anon") - if err != nil { - return err - } - a.AnonKey.Value = signed - } - // Generate service_role key if not provided - if len(a.ServiceRoleKey.Value) == 0 { - signed, err := a.generateJWT("service_role") - if err != nil { - return err - } - a.ServiceRoleKey.Value = signed - } - // Set hardcoded opaque keys - if len(a.PublishableKey.Value) == 0 { - a.PublishableKey.Value = defaultPublishableKey - } - if len(a.SecretKey.Value) == 0 { - a.SecretKey.Value = defaultSecretKey - } - return nil -} - -func (a auth) generateJWT(role string) (string, error) { - claims := CustomClaims{Issuer: "supabase-demo", Role: role} - if len(a.SigningKeysPath) > 0 && len(a.SigningKeys) > 0 { - claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 365 * 10)) // 10 years - return GenerateAsymmetricJWT(a.SigningKeys[0], claims) - } - // Fallback to generating symmetric keys - signed, err := claims.NewToken().SignedString([]byte(a.JwtSecret.Value)) - if err != nil { - return "", errors.Errorf("failed to generate JWT: %w", err) - } - return signed, nil -} - -// GenerateAsymmetricJWT generates a JWT token signed with the provided JWK private key -func GenerateAsymmetricJWT(jwk JWK, claims jwt.Claims) (string, error) { - privateKey, err := jwkToPrivateKey(jwk) - if err != nil { - return "", errors.Errorf("failed to convert JWK to private key: %w", err) - } - - // Determine signing method based on algorithm - var token *jwt.Token - switch jwk.Algorithm { - case AlgRS256: - token = jwt.NewWithClaims(jwt.SigningMethodRS256, claims) - case AlgES256: - token = jwt.NewWithClaims(jwt.SigningMethodES256, claims) - default: - return "", errors.Errorf("unsupported algorithm: %s", jwk.Algorithm) - } - - if len(jwk.KeyID) > 0 { - token.Header["kid"] = jwk.KeyID - } - - tokenString, err := token.SignedString(privateKey) - if err != nil { - return "", errors.Errorf("failed to sign JWT: %w", err) - } - - return tokenString, nil -} - -// jwkToPrivateKey converts a JWK to a crypto.PrivateKey -func jwkToPrivateKey(jwk JWK) (crypto.PrivateKey, error) { - switch jwk.KeyType { - case "RSA": - return jwkToRSAPrivateKey(jwk) - case "EC": - return jwkToECDSAPrivateKey(jwk) - default: - return nil, errors.Errorf("unsupported key type: %s", jwk.KeyType) - } -} - -// jwkToRSAPrivateKey converts a JWK to an RSA private key -func jwkToRSAPrivateKey(jwk JWK) (*rsa.PrivateKey, error) { - nBytes, err := base64.RawURLEncoding.DecodeString(jwk.Modulus) - if err != nil { - return nil, errors.Errorf("failed to decode modulus: %w", err) - } - n := new(big.Int).SetBytes(nBytes) - - eBytes, err := base64.RawURLEncoding.DecodeString(jwk.Exponent) - if err != nil { - return nil, errors.Errorf("failed to decode exponent: %w", err) - } - e := int(new(big.Int).SetBytes(eBytes).Int64()) - - dBytes, err := base64.RawURLEncoding.DecodeString(jwk.PrivateExponent) - if err != nil { - return nil, errors.Errorf("failed to decode private exponent: %w", err) - } - d := new(big.Int).SetBytes(dBytes) - - pBytes, err := base64.RawURLEncoding.DecodeString(jwk.FirstPrimeFactor) - if err != nil { - return nil, errors.Errorf("failed to decode first prime factor: %w", err) - } - p := new(big.Int).SetBytes(pBytes) - - qBytes, err := base64.RawURLEncoding.DecodeString(jwk.SecondPrimeFactor) - if err != nil { - return nil, errors.Errorf("failed to decode second prime factor: %w", err) - } - q := new(big.Int).SetBytes(qBytes) - - return &rsa.PrivateKey{ - PublicKey: rsa.PublicKey{N: n, E: e}, - D: d, - Primes: []*big.Int{p, q}, - }, nil -} - -// jwkToECDSAPrivateKey converts a JWK to an ECDSA private key -func jwkToECDSAPrivateKey(jwk JWK) (*ecdsa.PrivateKey, error) { - // Only support P-256 curve for ES256 - if jwk.Curve != "P-256" { - return nil, errors.Errorf("unsupported curve: %s", jwk.Curve) - } - - xBytes, err := base64.RawURLEncoding.DecodeString(jwk.X) - if err != nil { - return nil, errors.Errorf("failed to decode x coordinate: %w", err) - } - x := new(big.Int).SetBytes(xBytes) - - yBytes, err := base64.RawURLEncoding.DecodeString(jwk.Y) - if err != nil { - return nil, errors.Errorf("failed to decode y coordinate: %w", err) - } - y := new(big.Int).SetBytes(yBytes) - - dBytes, err := base64.RawURLEncoding.DecodeString(jwk.PrivateExponent) - if err != nil { - return nil, errors.Errorf("failed to decode private key: %w", err) - } - d := new(big.Int).SetBytes(dBytes) - - return &ecdsa.PrivateKey{ - PublicKey: ecdsa.PublicKey{ - Curve: elliptic.P256(), - X: x, - Y: y, - }, - D: d, - }, nil -} - -func NewBigIntFromBase64(n string) (*big.Int, error) { - nBytes, err := base64.RawURLEncoding.DecodeString(n) - if err != nil { - return nil, errors.Errorf("failed to decode base64: %w", err) - } - return new(big.Int).SetBytes(nBytes), nil -} diff --git a/apps/cli-go/pkg/config/auth.go b/apps/cli-go/pkg/config/auth.go deleted file mode 100644 index 75bbf609c3..0000000000 --- a/apps/cli-go/pkg/config/auth.go +++ /dev/null @@ -1,1727 +0,0 @@ -package config - -import ( - "slices" - "strconv" - "strings" - "time" - - "github.com/go-errors/errors" - "github.com/oapi-codegen/nullable" - openapi_types "github.com/oapi-codegen/runtime/types" - v1API "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/cast" - "github.com/supabase/cli/pkg/diff" -) - -type PasswordRequirements string - -const ( - NoRequirements PasswordRequirements = "" - LettersDigits PasswordRequirements = "letters_digits" - LowerUpperLettersDigits PasswordRequirements = "lower_upper_letters_digits" - LowerUpperLettersDigitsSymbols PasswordRequirements = "lower_upper_letters_digits_symbols" -) - -func (r *PasswordRequirements) UnmarshalText(text []byte) error { - allowed := []PasswordRequirements{NoRequirements, LettersDigits, LowerUpperLettersDigits, LowerUpperLettersDigitsSymbols} - if *r = PasswordRequirements(text); !slices.Contains(allowed, *r) { - return errors.Errorf("must be one of %v", allowed) - } - return nil -} - -func (r PasswordRequirements) ToChar() v1API.UpdateAuthConfigBodyPasswordRequiredCharacters { - switch r { - case LettersDigits: - return v1API.UpdateAuthConfigBodyPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 - case LowerUpperLettersDigits: - return v1API.UpdateAuthConfigBodyPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567891 - case LowerUpperLettersDigitsSymbols: - return v1API.UpdateAuthConfigBodyPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567892 - } - return v1API.UpdateAuthConfigBodyPasswordRequiredCharactersEmpty -} - -func NewPasswordRequirement(c v1API.UpdateAuthConfigBodyPasswordRequiredCharacters) PasswordRequirements { - switch c { - case v1API.UpdateAuthConfigBodyPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789: - return LettersDigits - case v1API.UpdateAuthConfigBodyPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567891: - return LowerUpperLettersDigits - case v1API.UpdateAuthConfigBodyPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567892: - return LowerUpperLettersDigitsSymbols - } - return NoRequirements -} - -type CaptchaProvider string - -const ( - HCaptchaProvider CaptchaProvider = "hcaptcha" - TurnstileProvider CaptchaProvider = "turnstile" -) - -func (p *CaptchaProvider) UnmarshalText(text []byte) error { - allowed := []CaptchaProvider{HCaptchaProvider, TurnstileProvider} - if *p = CaptchaProvider(text); !slices.Contains(allowed, *p) { - return errors.Errorf("must be one of %v", allowed) - } - return nil -} - -type Algorithm string - -const ( - AlgRS256 Algorithm = "RS256" - AlgES256 Algorithm = "ES256" -) - -func (p *Algorithm) UnmarshalText(text []byte) error { - allowed := []Algorithm{AlgRS256, AlgES256} - if *p = Algorithm(text); !slices.Contains(allowed, *p) { - return errors.Errorf("must be one of %v", allowed) - } - return nil -} - -type JWK struct { - KeyType string `toml:"kty" json:"kty"` - KeyID string `toml:"kid,omitempty" json:"kid,omitempty"` - Use string `toml:"use,omitempty" json:"use,omitempty"` - KeyOps []string `toml:"key_ops,omitempty" json:"key_ops,omitempty"` - Algorithm Algorithm `toml:"alg,omitempty" json:"alg,omitempty"` - Extractable *bool `toml:"ext,omitempty" json:"ext,omitempty"` - // RSA specific fields - Modulus string `toml:"n,omitempty" json:"n,omitempty"` - Exponent string `toml:"e,omitempty" json:"e,omitempty"` - // RSA private key fields - PrivateExponent string `toml:"d,omitempty" json:"d,omitempty"` - FirstPrimeFactor string `toml:"p,omitempty" json:"p,omitempty"` - SecondPrimeFactor string `toml:"q,omitempty" json:"q,omitempty"` - FirstFactorCRTExponent string `toml:"dp,omitempty" json:"dp,omitempty"` - SecondFactorCRTExponent string `toml:"dq,omitempty" json:"dq,omitempty"` - FirstCRTCoefficient string `toml:"qi,omitempty" json:"qi,omitempty"` - // EC specific fields - Curve string `toml:"crv,omitempty" json:"crv,omitempty"` - X string `toml:"x,omitempty" json:"x,omitempty"` - Y string `toml:"y,omitempty" json:"y,omitempty"` -} - -// ToPublicJWK converts a JWK to a public-only version by removing private key components -func (j JWK) ToPublicJWK() JWK { - publicJWK := JWK{ - KeyType: j.KeyType, - KeyID: j.KeyID, - Use: j.Use, - Algorithm: j.Algorithm, - } - - // Copy the underlying type instead of the pointer - if j.Extractable != nil { - publicJWK.Extractable = cast.Ptr(*j.Extractable) - } - - // Only include key_ops for verification (not signing) for public keys - for _, op := range j.KeyOps { - if op == "verify" { - publicJWK.KeyOps = append(publicJWK.KeyOps, op) - } - } - - switch j.KeyType { - case "RSA": - // Include only public key components for RSA - publicJWK.Modulus = j.Modulus - publicJWK.Exponent = j.Exponent - case "EC": - // Include only public key components for ECDSA - publicJWK.Curve = j.Curve - publicJWK.X = j.X - publicJWK.Y = j.Y - } - - return publicJWK -} - -type ( - auth struct { - Enabled bool `toml:"enabled" json:"enabled"` - Image string `toml:"-" json:"-"` - - SiteUrl string `toml:"site_url" json:"site_url"` - ExternalUrl string `toml:"external_url" json:"external_url"` - AdditionalRedirectUrls []string `toml:"additional_redirect_urls" json:"additional_redirect_urls"` - JwtExpiry uint `toml:"jwt_expiry" json:"jwt_expiry"` - JwtIssuer string `toml:"jwt_issuer" json:"jwt_issuer"` - EnableRefreshTokenRotation bool `toml:"enable_refresh_token_rotation" json:"enable_refresh_token_rotation"` - RefreshTokenReuseInterval uint `toml:"refresh_token_reuse_interval" json:"refresh_token_reuse_interval"` - EnableManualLinking bool `toml:"enable_manual_linking" json:"enable_manual_linking"` - EnableSignup bool `toml:"enable_signup" json:"enable_signup"` - EnableAnonymousSignIns bool `toml:"enable_anonymous_sign_ins" json:"enable_anonymous_sign_ins"` - MinimumPasswordLength uint `toml:"minimum_password_length" json:"minimum_password_length"` - PasswordRequirements PasswordRequirements `toml:"password_requirements" json:"password_requirements"` - SigningKeysPath string `toml:"signing_keys_path" json:"signing_keys_path"` - SigningKeys []JWK `toml:"-" json:"-"` - Passkey *Passkey `toml:"passkey" json:"passkey"` - Webauthn *Webauthn `toml:"webauthn" json:"webauthn"` - - RateLimit rateLimit `toml:"rate_limit" json:"rate_limit"` - Captcha *captcha `toml:"captcha" json:"captcha"` - Hook hook `toml:"hook" json:"hook"` - MFA mfa `toml:"mfa" json:"mfa"` - Sessions sessions `toml:"sessions" json:"sessions"` - Email email `toml:"email" json:"email"` - Sms sms `toml:"sms" json:"sms"` - External external `toml:"external" json:"external"` - Web3 web3 `toml:"web3" json:"web3"` - OAuthServer OAuthServer `toml:"oauth_server" json:"oauth_server"` - - // Custom secrets can be injected from .env file - PublishableKey Secret `toml:"publishable_key" json:"publishable_key"` - SecretKey Secret `toml:"secret_key" json:"secret_key"` - JwtSecret Secret `toml:"jwt_secret" json:"jwt_secret"` - AnonKey Secret `toml:"anon_key" json:"anon_key"` - ServiceRoleKey Secret `toml:"service_role_key" json:"service_role_key"` - - ThirdParty thirdParty `toml:"third_party" json:"third_party"` - } - - external map[string]provider - - thirdParty struct { - Firebase tpaFirebase `toml:"firebase" json:"firebase"` - Auth0 tpaAuth0 `toml:"auth0" json:"auth0"` - Cognito tpaCognito `toml:"aws_cognito" json:"aws_cognito"` - Clerk tpaClerk `toml:"clerk" json:"clerk"` - WorkOs tpaWorkOs `toml:"workos" json:"workos"` - } - - rateLimit struct { - AnonymousUsers uint `toml:"anonymous_users" json:"anonymous_users"` - TokenRefresh uint `toml:"token_refresh" json:"token_refresh"` - SignInSignUps uint `toml:"sign_in_sign_ups" json:"sign_in_sign_ups"` - TokenVerifications uint `toml:"token_verifications" json:"token_verifications"` - EmailSent uint `toml:"email_sent" json:"email_sent"` - SmsSent uint `toml:"sms_sent" json:"sms_sent"` - Web3 uint `toml:"web3" json:"web3"` - } - - tpaFirebase struct { - Enabled bool `toml:"enabled" json:"enabled"` - - ProjectID string `toml:"project_id" json:"project_id"` - } - - tpaAuth0 struct { - Enabled bool `toml:"enabled" json:"enabled"` - - Tenant string `toml:"tenant" json:"tenant"` - TenantRegion string `toml:"tenant_region" json:"tenant_region"` - } - - tpaCognito struct { - Enabled bool `toml:"enabled" json:"enabled"` - - UserPoolID string `toml:"user_pool_id" json:"user_pool_id"` - UserPoolRegion string `toml:"user_pool_region" json:"user_pool_region"` - } - - tpaClerk struct { - Enabled bool `toml:"enabled" json:"enabled"` - - Domain string `toml:"domain" json:"domain"` - } - - tpaWorkOs struct { - Enabled bool `toml:"enabled" json:"enabled"` - - IssuerUrl string `toml:"issuer_url" json:"issuer_url"` - } - - email struct { - EnableSignup bool `toml:"enable_signup" json:"enable_signup"` - DoubleConfirmChanges bool `toml:"double_confirm_changes" json:"double_confirm_changes"` - EnableConfirmations bool `toml:"enable_confirmations" json:"enable_confirmations"` - SecurePasswordChange bool `toml:"secure_password_change" json:"secure_password_change"` - Template map[string]emailTemplate `toml:"template" json:"template"` - Notification map[string]notification `toml:"notification" json:"notification"` - Smtp *smtp `toml:"smtp" json:"smtp"` - MaxFrequency time.Duration `toml:"max_frequency" json:"max_frequency"` - OtpLength uint `toml:"otp_length" json:"otp_length"` - OtpExpiry uint `toml:"otp_expiry" json:"otp_expiry"` - } - - smtp struct { - Enabled bool `toml:"enabled" json:"enabled"` - Host string `toml:"host" json:"host"` - Port uint16 `toml:"port" json:"port"` - User string `toml:"user" json:"user"` - Pass Secret `toml:"pass" json:"pass"` - AdminEmail openapi_types.Email `toml:"admin_email" json:"admin_email"` - SenderName string `toml:"sender_name" json:"sender_name"` - } - - emailTemplate struct { - Subject *string `toml:"subject" json:"subject"` - Content *string `toml:"content" json:"content"` - // Only content path is accepted in config.toml - ContentPath string `toml:"content_path" json:"content_path"` - } - - notification struct { - Enabled bool `toml:"enabled" json:"enabled"` - emailTemplate - } - - sms struct { - EnableSignup bool `toml:"enable_signup" json:"enable_signup"` - EnableConfirmations bool `toml:"enable_confirmations" json:"enable_confirmations"` - Template string `toml:"template" json:"template"` - Twilio twilioConfig `toml:"twilio" json:"twilio"` - TwilioVerify twilioConfig `toml:"twilio_verify" json:"twilio_verify"` - Messagebird messagebirdConfig `toml:"messagebird" json:"messagebird"` - Textlocal textlocalConfig `toml:"textlocal" json:"textlocal"` - Vonage vonageConfig `toml:"vonage" json:"vonage"` - TestOTP map[string]string `toml:"test_otp" json:"test_otp"` - MaxFrequency time.Duration `toml:"max_frequency" json:"max_frequency"` - } - - captcha struct { - Enabled bool `toml:"enabled" json:"enabled"` - Provider CaptchaProvider `toml:"provider" json:"provider"` - Secret Secret `toml:"secret" json:"secret"` - } - - hook struct { - MFAVerificationAttempt *hookConfig `toml:"mfa_verification_attempt" json:"mfa_verification_attempt"` - PasswordVerificationAttempt *hookConfig `toml:"password_verification_attempt" json:"password_verification_attempt"` - CustomAccessToken *hookConfig `toml:"custom_access_token" json:"custom_access_token"` - SendSMS *hookConfig `toml:"send_sms" json:"send_sms"` - SendEmail *hookConfig `toml:"send_email" json:"send_email"` - BeforeUserCreated *hookConfig `toml:"before_user_created" json:"before_user_created"` - } - - factorTypeConfiguration struct { - EnrollEnabled bool `toml:"enroll_enabled" json:"enroll_enabled"` - VerifyEnabled bool `toml:"verify_enabled" json:"verify_enabled"` - } - - phoneFactorTypeConfiguration struct { - factorTypeConfiguration - OtpLength uint `toml:"otp_length" json:"otp_length"` - Template string `toml:"template" json:"template"` - MaxFrequency time.Duration `toml:"max_frequency" json:"max_frequency"` - } - - mfa struct { - TOTP factorTypeConfiguration `toml:"totp" json:"totp"` - Phone phoneFactorTypeConfiguration `toml:"phone" json:"phone"` - WebAuthn factorTypeConfiguration `toml:"web_authn" json:"web_authn"` - MaxEnrolledFactors uint `toml:"max_enrolled_factors" json:"max_enrolled_factors"` - } - - hookConfig struct { - Enabled bool `toml:"enabled" json:"enabled"` - URI string `toml:"uri" json:"uri"` - Secrets Secret `toml:"secrets" json:"secrets"` - } - - sessions struct { - Timebox time.Duration `toml:"timebox" json:"timebox"` - InactivityTimeout time.Duration `toml:"inactivity_timeout" json:"inactivity_timeout"` - } - - twilioConfig struct { - Enabled bool `toml:"enabled" json:"enabled"` - AccountSid string `toml:"account_sid" json:"account_sid"` - MessageServiceSid string `toml:"message_service_sid" json:"message_service_sid"` - AuthToken Secret `toml:"auth_token" json:"auth_token"` - } - - messagebirdConfig struct { - Enabled bool `toml:"enabled" json:"enabled"` - Originator string `toml:"originator" json:"originator"` - AccessKey Secret `toml:"access_key" json:"access_key"` - } - - textlocalConfig struct { - Enabled bool `toml:"enabled" json:"enabled"` - Sender string `toml:"sender" json:"sender"` - ApiKey Secret `toml:"api_key" json:"api_key"` - } - - vonageConfig struct { - Enabled bool `toml:"enabled" json:"enabled"` - From string `toml:"from" json:"from"` - ApiKey string `toml:"api_key" json:"api_key"` - ApiSecret Secret `toml:"api_secret" json:"api_secret"` - } - - provider struct { - Enabled bool `toml:"enabled" json:"enabled"` - ClientId string `toml:"client_id" json:"client_id"` - Secret Secret `toml:"secret" json:"secret"` - Url string `toml:"url" json:"url"` - RedirectUri string `toml:"redirect_uri" json:"redirect_uri"` - SkipNonceCheck bool `toml:"skip_nonce_check" json:"skip_nonce_check"` - EmailOptional bool `toml:"email_optional" json:"email_optional"` - } - - solana struct { - Enabled bool `toml:"enabled" json:"enabled"` - } - - ethereum struct { - Enabled bool `toml:"enabled" json:"enabled"` - } - - web3 struct { - Solana solana `toml:"solana" json:"solana"` - Ethereum ethereum `toml:"ethereum" json:"ethereum"` - } - - Passkey struct { - Enabled bool `toml:"enabled" json:"enabled"` - } - - Webauthn struct { - RpDisplayName string `toml:"rp_display_name" json:"rp_display_name"` - RpId string `toml:"rp_id" json:"rp_id"` - RpOrigins []string `toml:"rp_origins" json:"rp_origins"` - } - - OAuthServer struct { - Enabled bool `toml:"enabled" json:"enabled"` - AllowDynamicRegistration bool `toml:"allow_dynamic_registration" json:"allow_dynamic_registration"` - AuthorizationUrlPath string `toml:"authorization_url_path" json:"authorization_url_path"` - } -) - -func (a auth) GetExternalURL(apiExternalURL string) string { - if len(a.ExternalUrl) > 0 { - return a.ExternalUrl - } - return strings.TrimRight(apiExternalURL, "/") + "/auth/v1" -} - -func (a *auth) ToUpdateAuthConfigBody() v1API.UpdateAuthConfigBody { - body := v1API.UpdateAuthConfigBody{ - SiteUrl: nullable.NewNullableWithValue(a.SiteUrl), - UriAllowList: nullable.NewNullableWithValue(strings.Join(a.AdditionalRedirectUrls, ",")), - JwtExp: nullable.NewNullableWithValue(cast.UintToInt(a.JwtExpiry)), - RefreshTokenRotationEnabled: nullable.NewNullableWithValue(a.EnableRefreshTokenRotation), - SecurityRefreshTokenReuseInterval: nullable.NewNullableWithValue(cast.UintToInt(a.RefreshTokenReuseInterval)), - SecurityManualLinkingEnabled: nullable.NewNullableWithValue(a.EnableManualLinking), - DisableSignup: nullable.NewNullableWithValue(!a.EnableSignup), - ExternalAnonymousUsersEnabled: nullable.NewNullableWithValue(a.EnableAnonymousSignIns), - PasswordMinLength: nullable.NewNullableWithValue(cast.UintToInt(a.MinimumPasswordLength)), - PasswordRequiredCharacters: nullable.NewNullableWithValue(a.PasswordRequirements.ToChar()), - } - // Add rate limit fields - a.RateLimit.toAuthConfigBody(&body) - if s := a.Email.Smtp; s != nil && s.Enabled { - body.RateLimitEmailSent = nullable.NewNullableWithValue(cast.UintToInt(a.RateLimit.EmailSent)) - } - // When local config is not set, we assume platform defaults should not change - if a.Captcha != nil { - a.Captcha.toAuthConfigBody(&body) - } - if a.Passkey != nil { - a.Passkey.toAuthConfigBody(&body) - } - if a.Webauthn != nil { - a.Webauthn.toAuthConfigBody(&body) - } - a.Hook.toAuthConfigBody(&body) - a.MFA.toAuthConfigBody(&body) - a.Sessions.toAuthConfigBody(&body) - a.Email.toAuthConfigBody(&body) - a.Sms.toAuthConfigBody(&body) - a.External.toAuthConfigBody(&body) - a.Web3.toAuthConfigBody(&body) - a.OAuthServer.toAuthConfigBody(&body) - return body -} - -func (a *auth) FromRemoteAuthConfig(remoteConfig v1API.AuthConfigResponseOutput) { - a.SiteUrl = ValOrDefault(remoteConfig.SiteUrl, "") - a.AdditionalRedirectUrls = strToArr(ValOrDefault(remoteConfig.UriAllowList, "")) - a.JwtExpiry = cast.IntToUint(ValOrDefault(remoteConfig.JwtExp, 0)) - a.EnableRefreshTokenRotation = ValOrDefault(remoteConfig.RefreshTokenRotationEnabled, false) - a.RefreshTokenReuseInterval = cast.IntToUint(ValOrDefault(remoteConfig.SecurityRefreshTokenReuseInterval, 0)) - a.EnableManualLinking = ValOrDefault(remoteConfig.SecurityManualLinkingEnabled, false) - a.EnableSignup = !ValOrDefault(remoteConfig.DisableSignup, false) - a.EnableAnonymousSignIns = ValOrDefault(remoteConfig.ExternalAnonymousUsersEnabled, false) - a.MinimumPasswordLength = cast.IntToUint(ValOrDefault(remoteConfig.PasswordMinLength, 0)) - prc := ValOrDefault(remoteConfig.PasswordRequiredCharacters, "") - a.PasswordRequirements = NewPasswordRequirement(v1API.UpdateAuthConfigBodyPasswordRequiredCharacters(prc)) - a.Passkey.fromAuthConfig(remoteConfig) - a.Webauthn.fromAuthConfig(remoteConfig) - a.RateLimit.fromAuthConfig(remoteConfig) - if s := a.Email.Smtp; s != nil && s.Enabled { - a.RateLimit.EmailSent = cast.IntToUint(ValOrDefault(remoteConfig.RateLimitEmailSent, 0)) - } - a.Captcha.fromAuthConfig(remoteConfig) - a.Hook.fromAuthConfig(remoteConfig) - a.MFA.fromAuthConfig(remoteConfig) - a.Sessions.fromAuthConfig(remoteConfig) - a.Email.fromAuthConfig(remoteConfig) - a.Sms.fromAuthConfig(remoteConfig) - a.External.fromAuthConfig(remoteConfig) - a.Web3.fromAuthConfig(remoteConfig) - a.OAuthServer.fromAuthConfig(remoteConfig) -} - -func (r rateLimit) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) { - body.RateLimitAnonymousUsers = nullable.NewNullableWithValue(cast.UintToInt(r.AnonymousUsers)) - body.RateLimitTokenRefresh = nullable.NewNullableWithValue(cast.UintToInt(r.TokenRefresh)) - body.RateLimitOtp = nullable.NewNullableWithValue(cast.UintToInt(r.SignInSignUps)) - body.RateLimitVerify = nullable.NewNullableWithValue(cast.UintToInt(r.TokenVerifications)) - // Email rate limit is only updated when SMTP is enabled - body.RateLimitSmsSent = nullable.NewNullableWithValue(cast.UintToInt(r.SmsSent)) - body.RateLimitWeb3 = nullable.NewNullableWithValue((cast.UintToInt(r.Web3))) -} - -func (r *rateLimit) fromAuthConfig(remoteConfig v1API.AuthConfigResponseOutput) { - r.AnonymousUsers = cast.IntToUint(ValOrDefault(remoteConfig.RateLimitAnonymousUsers, 0)) - r.TokenRefresh = cast.IntToUint(ValOrDefault(remoteConfig.RateLimitTokenRefresh, 0)) - r.SignInSignUps = cast.IntToUint(ValOrDefault(remoteConfig.RateLimitOtp, 0)) - r.TokenVerifications = cast.IntToUint(ValOrDefault(remoteConfig.RateLimitVerify, 0)) - // Email rate limit is only updated when SMTP is enabled - r.SmsSent = cast.IntToUint(ValOrDefault(remoteConfig.RateLimitSmsSent, 0)) - r.Web3 = cast.IntToUint(ValOrDefault(remoteConfig.RateLimitWeb3, 0)) -} - -func (c captcha) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) { - if body.SecurityCaptchaEnabled = nullable.NewNullableWithValue(c.Enabled); c.Enabled { - body.SecurityCaptchaProvider = nullable.NewNullableWithValue(v1API.UpdateAuthConfigBodySecurityCaptchaProvider(c.Provider)) - if len(c.Secret.SHA256) > 0 { - body.SecurityCaptchaSecret = nullable.NewNullableWithValue(c.Secret.Value) - } - } -} - -func (c *captcha) fromAuthConfig(remoteConfig v1API.AuthConfigResponseOutput) { - // When local config is not set, we assume platform defaults should not change - if c == nil { - return - } - // Ignore disabled captcha fields to minimise config diff - if c.Enabled { - c.Provider = CaptchaProvider(ValOrDefault(remoteConfig.SecurityCaptchaProvider, "")) - if len(c.Secret.SHA256) > 0 { - c.Secret.SHA256 = ValOrDefault(remoteConfig.SecurityCaptchaSecret, "") - } - } - c.Enabled = ValOrDefault(remoteConfig.SecurityCaptchaEnabled, false) -} - -func (p Passkey) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) { - body.PasskeyEnabled = cast.Ptr(p.Enabled) -} - -func (p *Passkey) fromAuthConfig(remoteConfig v1API.AuthConfigResponseOutput) { - // When local config is not set, we assume platform defaults should not change - if p == nil { - return - } - p.Enabled = remoteConfig.PasskeyEnabled -} - -func (w Webauthn) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) { - body.WebauthnRpDisplayName = nullable.NewNullableWithValue(w.RpDisplayName) - body.WebauthnRpId = nullable.NewNullableWithValue(w.RpId) - body.WebauthnRpOrigins = nullable.NewNullableWithValue(strings.Join(w.RpOrigins, ",")) -} - -func (w *Webauthn) fromAuthConfig(remoteConfig v1API.AuthConfigResponseOutput) { - // When local config is not set, we assume platform defaults should not change - if w == nil { - return - } - w.RpDisplayName = ValOrDefault(remoteConfig.WebauthnRpDisplayName, "") - w.RpId = ValOrDefault(remoteConfig.WebauthnRpId, "") - w.RpOrigins = strToArr(ValOrDefault(remoteConfig.WebauthnRpOrigins, "")) -} - -func (h hook) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) { - // When local config is not set, we assume platform defaults should not change - if hook := h.BeforeUserCreated; hook != nil { - if body.HookBeforeUserCreatedEnabled = nullable.NewNullableWithValue(hook.Enabled); hook.Enabled { - body.HookBeforeUserCreatedUri = nullable.NewNullableWithValue(hook.URI) - if len(hook.Secrets.SHA256) > 0 { - body.HookBeforeUserCreatedSecrets = nullable.NewNullableWithValue(hook.Secrets.Value) - } - } - } - if hook := h.CustomAccessToken; hook != nil { - if body.HookCustomAccessTokenEnabled = nullable.NewNullableWithValue(hook.Enabled); hook.Enabled { - body.HookCustomAccessTokenUri = nullable.NewNullableWithValue(hook.URI) - if len(hook.Secrets.SHA256) > 0 { - body.HookCustomAccessTokenSecrets = nullable.NewNullableWithValue(hook.Secrets.Value) - } - } - } - if hook := h.SendEmail; hook != nil { - if body.HookSendEmailEnabled = nullable.NewNullableWithValue(hook.Enabled); hook.Enabled { - body.HookSendEmailUri = nullable.NewNullableWithValue(hook.URI) - if len(hook.Secrets.SHA256) > 0 { - body.HookSendEmailSecrets = nullable.NewNullableWithValue(hook.Secrets.Value) - } - } - } - if hook := h.SendSMS; hook != nil { - if body.HookSendSmsEnabled = nullable.NewNullableWithValue(hook.Enabled); hook.Enabled { - body.HookSendSmsUri = nullable.NewNullableWithValue(hook.URI) - if len(hook.Secrets.SHA256) > 0 { - body.HookSendSmsSecrets = nullable.NewNullableWithValue(hook.Secrets.Value) - } - } - } - // Enterprise and team only features - if hook := h.MFAVerificationAttempt; hook != nil { - if body.HookMfaVerificationAttemptEnabled = nullable.NewNullableWithValue(hook.Enabled); hook.Enabled { - body.HookMfaVerificationAttemptUri = nullable.NewNullableWithValue(hook.URI) - if len(hook.Secrets.SHA256) > 0 { - body.HookMfaVerificationAttemptSecrets = nullable.NewNullableWithValue(hook.Secrets.Value) - } - } - } - if hook := h.PasswordVerificationAttempt; hook != nil { - if body.HookPasswordVerificationAttemptEnabled = nullable.NewNullableWithValue(hook.Enabled); hook.Enabled { - body.HookPasswordVerificationAttemptUri = nullable.NewNullableWithValue(hook.URI) - if len(hook.Secrets.SHA256) > 0 { - body.HookPasswordVerificationAttemptSecrets = nullable.NewNullableWithValue(hook.Secrets.Value) - } - } - } -} -func (h *hook) fromAuthConfig(remoteConfig v1API.AuthConfigResponseOutput) { - // When local config is not set, we assume platform defaults should not change - if hook := h.BeforeUserCreated; hook != nil { - // Ignore disabled hooks because their envs are not loaded - if hook.Enabled { - hook.URI = ValOrDefault(remoteConfig.HookBeforeUserCreatedUri, "") - if len(hook.Secrets.SHA256) > 0 { - hook.Secrets.SHA256 = ValOrDefault(remoteConfig.HookBeforeUserCreatedSecrets, "") - } - } - hook.Enabled = ValOrDefault(remoteConfig.HookBeforeUserCreatedEnabled, false) - } - if hook := h.CustomAccessToken; hook != nil { - // Ignore disabled hooks because their envs are not loaded - if hook.Enabled { - hook.URI = ValOrDefault(remoteConfig.HookCustomAccessTokenUri, "") - if len(hook.Secrets.SHA256) > 0 { - hook.Secrets.SHA256 = ValOrDefault(remoteConfig.HookCustomAccessTokenSecrets, "") - } - } - hook.Enabled = ValOrDefault(remoteConfig.HookCustomAccessTokenEnabled, false) - } - if hook := h.SendEmail; hook != nil { - if hook.Enabled { - hook.URI = ValOrDefault(remoteConfig.HookSendEmailUri, "") - if len(hook.Secrets.SHA256) > 0 { - hook.Secrets.SHA256 = ValOrDefault(remoteConfig.HookSendEmailSecrets, "") - } - } - hook.Enabled = ValOrDefault(remoteConfig.HookSendEmailEnabled, false) - } - if hook := h.SendSMS; hook != nil { - if hook.Enabled { - hook.URI = ValOrDefault(remoteConfig.HookSendSmsUri, "") - if len(hook.Secrets.SHA256) > 0 { - hook.Secrets.SHA256 = ValOrDefault(remoteConfig.HookSendSmsSecrets, "") - } - } - hook.Enabled = ValOrDefault(remoteConfig.HookSendSmsEnabled, false) - } - // Enterprise and team only features - if hook := h.MFAVerificationAttempt; hook != nil { - if hook.Enabled { - hook.URI = ValOrDefault(remoteConfig.HookMfaVerificationAttemptUri, "") - if len(hook.Secrets.SHA256) > 0 { - hook.Secrets.SHA256 = ValOrDefault(remoteConfig.HookMfaVerificationAttemptSecrets, "") - } - } - hook.Enabled = ValOrDefault(remoteConfig.HookMfaVerificationAttemptEnabled, false) - } - if hook := h.PasswordVerificationAttempt; hook != nil { - if hook.Enabled { - hook.URI = ValOrDefault(remoteConfig.HookPasswordVerificationAttemptUri, "") - if len(hook.Secrets.SHA256) > 0 { - hook.Secrets.SHA256 = ValOrDefault(remoteConfig.HookPasswordVerificationAttemptSecrets, "") - } - } - hook.Enabled = ValOrDefault(remoteConfig.HookPasswordVerificationAttemptEnabled, false) - } -} - -func (m mfa) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) { - body.MfaMaxEnrolledFactors = nullable.NewNullableWithValue(cast.UintToInt(m.MaxEnrolledFactors)) - body.MfaTotpEnrollEnabled = nullable.NewNullableWithValue(m.TOTP.EnrollEnabled) - body.MfaTotpVerifyEnabled = nullable.NewNullableWithValue(m.TOTP.VerifyEnabled) - body.MfaPhoneEnrollEnabled = nullable.NewNullableWithValue(m.Phone.EnrollEnabled) - body.MfaPhoneVerifyEnabled = nullable.NewNullableWithValue(m.Phone.VerifyEnabled) - body.MfaPhoneOtpLength = nullable.NewNullableWithValue(cast.UintToInt(m.Phone.OtpLength)) - body.MfaPhoneTemplate = nullable.NewNullableWithValue(m.Phone.Template) - body.MfaPhoneMaxFrequency = nullable.NewNullableWithValue(int(m.Phone.MaxFrequency.Seconds())) - body.MfaWebAuthnEnrollEnabled = nullable.NewNullableWithValue(m.WebAuthn.EnrollEnabled) - body.MfaWebAuthnVerifyEnabled = nullable.NewNullableWithValue(m.WebAuthn.VerifyEnabled) -} - -func (m *mfa) fromAuthConfig(remoteConfig v1API.AuthConfigResponseOutput) { - m.MaxEnrolledFactors = cast.IntToUint(ValOrDefault(remoteConfig.MfaMaxEnrolledFactors, 0)) - m.TOTP.EnrollEnabled = ValOrDefault(remoteConfig.MfaTotpEnrollEnabled, false) - m.TOTP.VerifyEnabled = ValOrDefault(remoteConfig.MfaTotpVerifyEnabled, false) - m.Phone.EnrollEnabled = ValOrDefault(remoteConfig.MfaPhoneEnrollEnabled, false) - m.Phone.VerifyEnabled = ValOrDefault(remoteConfig.MfaPhoneVerifyEnabled, false) - m.Phone.OtpLength = cast.IntToUint(remoteConfig.MfaPhoneOtpLength) - m.Phone.Template = ValOrDefault(remoteConfig.MfaPhoneTemplate, "") - m.Phone.MaxFrequency = time.Duration(ValOrDefault(remoteConfig.MfaPhoneMaxFrequency, 0)) * time.Second - m.WebAuthn.EnrollEnabled = ValOrDefault(remoteConfig.MfaWebAuthnEnrollEnabled, false) - m.WebAuthn.VerifyEnabled = ValOrDefault(remoteConfig.MfaWebAuthnVerifyEnabled, false) -} - -func (s sessions) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) { - body.SessionsTimebox = nullable.NewNullableWithValue(float32(s.Timebox.Hours())) - body.SessionsInactivityTimeout = nullable.NewNullableWithValue(float32(s.InactivityTimeout.Hours())) -} - -func (s *sessions) fromAuthConfig(remoteConfig v1API.AuthConfigResponseOutput) { - s.Timebox = time.Duration(ValOrDefault(remoteConfig.SessionsTimebox, 0)) * time.Hour - s.InactivityTimeout = time.Duration(ValOrDefault(remoteConfig.SessionsInactivityTimeout, 0)) * time.Hour -} - -func (e email) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) { - body.ExternalEmailEnabled = nullable.NewNullableWithValue(e.EnableSignup) - body.MailerSecureEmailChangeEnabled = nullable.NewNullableWithValue(e.DoubleConfirmChanges) - body.MailerAutoconfirm = nullable.NewNullableWithValue(!e.EnableConfirmations) - body.MailerOtpLength = nullable.NewNullableWithValue(cast.UintToInt(e.OtpLength)) - body.MailerOtpExp = cast.UintToIntPtr(&e.OtpExpiry) - body.SecurityUpdatePasswordRequireReauthentication = nullable.NewNullableWithValue(e.SecurePasswordChange) - body.SmtpMaxFrequency = nullable.NewNullableWithValue(int(e.MaxFrequency.Seconds())) - // When local config is not set, we assume platform defaults should not change - if e.Smtp != nil { - e.Smtp.toAuthConfigBody(body) - } - if len(e.Template) > 0 { - var tmpl *emailTemplate - tmpl = cast.Ptr(e.Template["invite"]) - if tmpl.Subject != nil { - body.MailerSubjectsInvite = nullable.NewNullableWithValue(*tmpl.Subject) - } - if tmpl.Content != nil { - body.MailerTemplatesInviteContent = nullable.NewNullableWithValue(*tmpl.Content) - } - tmpl = cast.Ptr(e.Template["confirmation"]) - if tmpl.Subject != nil { - body.MailerSubjectsConfirmation = nullable.NewNullableWithValue(*tmpl.Subject) - } - if tmpl.Content != nil { - body.MailerTemplatesConfirmationContent = nullable.NewNullableWithValue(*tmpl.Content) - } - tmpl = cast.Ptr(e.Template["recovery"]) - if tmpl.Subject != nil { - body.MailerSubjectsRecovery = nullable.NewNullableWithValue(*tmpl.Subject) - } - if tmpl.Content != nil { - body.MailerTemplatesRecoveryContent = nullable.NewNullableWithValue(*tmpl.Content) - } - tmpl = cast.Ptr(e.Template["magic_link"]) - if tmpl.Subject != nil { - body.MailerSubjectsMagicLink = nullable.NewNullableWithValue(*tmpl.Subject) - } - if tmpl.Content != nil { - body.MailerTemplatesMagicLinkContent = nullable.NewNullableWithValue(*tmpl.Content) - } - tmpl = cast.Ptr(e.Template["email_change"]) - if tmpl.Subject != nil { - body.MailerSubjectsEmailChange = nullable.NewNullableWithValue(*tmpl.Subject) - } - if tmpl.Content != nil { - body.MailerTemplatesEmailChangeContent = nullable.NewNullableWithValue(*tmpl.Content) - } - tmpl = cast.Ptr(e.Template["reauthentication"]) - if tmpl.Subject != nil { - body.MailerSubjectsReauthentication = nullable.NewNullableWithValue(*tmpl.Subject) - } - if tmpl.Content != nil { - body.MailerTemplatesReauthenticationContent = nullable.NewNullableWithValue(*tmpl.Content) - } - } - // Notifications - if len(e.Notification) > 0 { - if n, ok := e.Notification["password_changed"]; ok { - body.MailerNotificationsPasswordChangedEnabled = nullable.NewNullableWithValue(n.Enabled) - if n.Subject != nil { - body.MailerSubjectsPasswordChangedNotification = nullable.NewNullableWithValue(*n.Subject) - } - if n.Content != nil { - body.MailerTemplatesPasswordChangedNotificationContent = nullable.NewNullableWithValue(*n.Content) - } - } - if n, ok := e.Notification["email_changed"]; ok { - body.MailerNotificationsEmailChangedEnabled = nullable.NewNullableWithValue(n.Enabled) - if n.Subject != nil { - body.MailerSubjectsEmailChangedNotification = nullable.NewNullableWithValue(*n.Subject) - } - if n.Content != nil { - body.MailerTemplatesEmailChangedNotificationContent = nullable.NewNullableWithValue(*n.Content) - } - } - if n, ok := e.Notification["phone_changed"]; ok { - body.MailerNotificationsPhoneChangedEnabled = nullable.NewNullableWithValue(n.Enabled) - if n.Subject != nil { - body.MailerSubjectsPhoneChangedNotification = nullable.NewNullableWithValue(*n.Subject) - } - if n.Content != nil { - body.MailerTemplatesPhoneChangedNotificationContent = nullable.NewNullableWithValue(*n.Content) - } - } - if n, ok := e.Notification["identity_linked"]; ok { - body.MailerNotificationsIdentityLinkedEnabled = nullable.NewNullableWithValue(n.Enabled) - if n.Subject != nil { - body.MailerSubjectsIdentityLinkedNotification = nullable.NewNullableWithValue(*n.Subject) - } - if n.Content != nil { - body.MailerTemplatesIdentityLinkedNotificationContent = nullable.NewNullableWithValue(*n.Content) - } - } - if n, ok := e.Notification["identity_unlinked"]; ok { - body.MailerNotificationsIdentityUnlinkedEnabled = nullable.NewNullableWithValue(n.Enabled) - if n.Subject != nil { - body.MailerSubjectsIdentityUnlinkedNotification = nullable.NewNullableWithValue(*n.Subject) - } - if n.Content != nil { - body.MailerTemplatesIdentityUnlinkedNotificationContent = nullable.NewNullableWithValue(*n.Content) - } - } - if n, ok := e.Notification["mfa_factor_enrolled"]; ok { - body.MailerNotificationsMfaFactorEnrolledEnabled = nullable.NewNullableWithValue(n.Enabled) - if n.Subject != nil { - body.MailerSubjectsMfaFactorEnrolledNotification = nullable.NewNullableWithValue(*n.Subject) - } - if n.Content != nil { - body.MailerTemplatesMfaFactorEnrolledNotificationContent = nullable.NewNullableWithValue(*n.Content) - } - } - if n, ok := e.Notification["mfa_factor_unenrolled"]; ok { - body.MailerNotificationsMfaFactorUnenrolledEnabled = nullable.NewNullableWithValue(n.Enabled) - if n.Subject != nil { - body.MailerSubjectsMfaFactorUnenrolledNotification = nullable.NewNullableWithValue(*n.Subject) - } - if n.Content != nil { - body.MailerTemplatesMfaFactorUnenrolledNotificationContent = nullable.NewNullableWithValue(*n.Content) - } - } - } -} - -func (e *email) fromAuthConfig(remoteConfig v1API.AuthConfigResponseOutput) { - e.EnableSignup = ValOrDefault(remoteConfig.ExternalEmailEnabled, false) - e.DoubleConfirmChanges = ValOrDefault(remoteConfig.MailerSecureEmailChangeEnabled, false) - e.EnableConfirmations = !ValOrDefault(remoteConfig.MailerAutoconfirm, false) - e.OtpLength = cast.IntToUint(ValOrDefault(remoteConfig.MailerOtpLength, 0)) - e.OtpExpiry = cast.IntToUint(remoteConfig.MailerOtpExp) - e.SecurePasswordChange = ValOrDefault(remoteConfig.SecurityUpdatePasswordRequireReauthentication, false) - e.MaxFrequency = time.Duration(ValOrDefault(remoteConfig.SmtpMaxFrequency, 0)) * time.Second - e.Smtp.fromAuthConfig(remoteConfig) - if len(e.Template) > 0 { - if t, ok := e.Template["invite"]; ok { - if t.Subject != nil { - if value, err := remoteConfig.MailerSubjectsInvite.Get(); err == nil { - t.Subject = &value - } else { - t.Subject = nil - } - } - if t.Content != nil { - if value, err := remoteConfig.MailerTemplatesInviteContent.Get(); err == nil { - t.Content = &value - } else { - t.Content = nil - } - } - e.Template["invite"] = t - } - if t, ok := e.Template["confirmation"]; ok { - if t.Subject != nil { - if value, err := remoteConfig.MailerSubjectsConfirmation.Get(); err == nil { - t.Subject = &value - } else { - t.Subject = nil - } - } - if t.Content != nil { - if value, err := remoteConfig.MailerTemplatesConfirmationContent.Get(); err == nil { - t.Content = &value - } else { - t.Content = nil - } - } - e.Template["confirmation"] = t - } - if t, ok := e.Template["recovery"]; ok { - if t.Subject != nil { - if value, err := remoteConfig.MailerSubjectsRecovery.Get(); err == nil { - t.Subject = &value - } else { - t.Subject = nil - } - } - if t.Content != nil { - if value, err := remoteConfig.MailerTemplatesRecoveryContent.Get(); err == nil { - t.Content = &value - } else { - t.Content = nil - } - } - e.Template["recovery"] = t - } - if t, ok := e.Template["magic_link"]; ok { - if t.Subject != nil { - if value, err := remoteConfig.MailerSubjectsMagicLink.Get(); err == nil { - t.Subject = &value - } else { - t.Subject = nil - } - } - if t.Content != nil { - if value, err := remoteConfig.MailerTemplatesMagicLinkContent.Get(); err == nil { - t.Content = &value - } else { - t.Content = nil - } - } - e.Template["magic_link"] = t - } - if t, ok := e.Template["email_change"]; ok { - if t.Subject != nil { - if value, err := remoteConfig.MailerSubjectsEmailChange.Get(); err == nil { - t.Subject = &value - } else { - t.Subject = nil - } - } - if t.Content != nil { - if value, err := remoteConfig.MailerTemplatesEmailChangeContent.Get(); err == nil { - t.Content = &value - } else { - t.Content = nil - } - } - e.Template["email_change"] = t - } - if t, ok := e.Template["reauthentication"]; ok { - if t.Subject != nil { - if value, err := remoteConfig.MailerSubjectsReauthentication.Get(); err == nil { - t.Subject = &value - } else { - t.Subject = nil - } - } - if t.Content != nil { - if value, err := remoteConfig.MailerTemplatesReauthenticationContent.Get(); err == nil { - t.Content = &value - } else { - t.Content = nil - } - } - e.Template["reauthentication"] = t - } - } - // Notifications - if len(e.Notification) > 0 { - if n, ok := e.Notification["password_changed"]; ok { - if value, err := remoteConfig.MailerNotificationsPasswordChangedEnabled.Get(); err == nil { - n.Enabled = value - } - if n.Subject != nil { - if value, err := remoteConfig.MailerSubjectsPasswordChangedNotification.Get(); err == nil { - n.Subject = &value - } else { - n.Subject = nil - } - } - if n.Content != nil { - if value, err := remoteConfig.MailerTemplatesPasswordChangedNotificationContent.Get(); err == nil { - n.Content = &value - } else { - n.Content = nil - } - } - e.Notification["password_changed"] = n - } - if n, ok := e.Notification["email_changed"]; ok { - if value, err := remoteConfig.MailerNotificationsEmailChangedEnabled.Get(); err == nil { - n.Enabled = value - } - if n.Subject != nil { - if value, err := remoteConfig.MailerSubjectsEmailChangedNotification.Get(); err == nil { - n.Subject = &value - } else { - n.Subject = nil - } - } - if n.Content != nil { - if value, err := remoteConfig.MailerTemplatesEmailChangedNotificationContent.Get(); err == nil { - n.Content = &value - } else { - n.Content = nil - } - } - e.Notification["email_changed"] = n - } - if n, ok := e.Notification["phone_changed"]; ok { - if value, err := remoteConfig.MailerNotificationsPhoneChangedEnabled.Get(); err == nil { - n.Enabled = value - } - if n.Subject != nil { - if value, err := remoteConfig.MailerSubjectsPhoneChangedNotification.Get(); err == nil { - n.Subject = &value - } else { - n.Subject = nil - } - } - if n.Content != nil { - if value, err := remoteConfig.MailerTemplatesPhoneChangedNotificationContent.Get(); err == nil { - n.Content = &value - } else { - n.Content = nil - } - } - e.Notification["phone_changed"] = n - } - if n, ok := e.Notification["identity_linked"]; ok { - if value, err := remoteConfig.MailerNotificationsIdentityLinkedEnabled.Get(); err == nil { - n.Enabled = value - } - if n.Subject != nil { - if value, err := remoteConfig.MailerSubjectsIdentityLinkedNotification.Get(); err == nil { - n.Subject = &value - } else { - n.Subject = nil - } - } - if n.Content != nil { - if value, err := remoteConfig.MailerTemplatesIdentityLinkedNotificationContent.Get(); err == nil { - n.Content = &value - } else { - n.Content = nil - } - } - e.Notification["identity_linked"] = n - } - if n, ok := e.Notification["identity_unlinked"]; ok { - if value, err := remoteConfig.MailerNotificationsIdentityUnlinkedEnabled.Get(); err == nil { - n.Enabled = value - } - if n.Subject != nil { - if value, err := remoteConfig.MailerSubjectsIdentityUnlinkedNotification.Get(); err == nil { - n.Subject = &value - } else { - n.Subject = nil - } - } - if n.Content != nil { - if value, err := remoteConfig.MailerTemplatesIdentityUnlinkedNotificationContent.Get(); err == nil { - n.Content = &value - } else { - n.Content = nil - } - } - e.Notification["identity_unlinked"] = n - } - if n, ok := e.Notification["mfa_factor_enrolled"]; ok { - if value, err := remoteConfig.MailerNotificationsMfaFactorEnrolledEnabled.Get(); err == nil { - n.Enabled = value - } - if n.Subject != nil { - if value, err := remoteConfig.MailerSubjectsMfaFactorEnrolledNotification.Get(); err == nil { - n.Subject = &value - } else { - n.Subject = nil - } - } - if n.Content != nil { - if value, err := remoteConfig.MailerTemplatesMfaFactorEnrolledNotificationContent.Get(); err == nil { - n.Content = &value - } else { - n.Content = nil - } - } - e.Notification["mfa_factor_enrolled"] = n - } - if n, ok := e.Notification["mfa_factor_unenrolled"]; ok { - if value, err := remoteConfig.MailerNotificationsMfaFactorUnenrolledEnabled.Get(); err == nil { - n.Enabled = value - } - if n.Subject != nil { - if value, err := remoteConfig.MailerSubjectsMfaFactorUnenrolledNotification.Get(); err == nil { - n.Subject = &value - } else { - n.Subject = nil - } - } - if n.Content != nil { - if value, err := remoteConfig.MailerTemplatesMfaFactorUnenrolledNotificationContent.Get(); err == nil { - n.Content = &value - } else { - n.Content = nil - } - } - e.Notification["mfa_factor_unenrolled"] = n - } - } -} - -func (s smtp) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) { - if !s.Enabled { - // Setting a single empty string disables SMTP - body.SmtpHost = nullable.NewNullableWithValue("") - return - } - body.SmtpHost = nullable.NewNullableWithValue(s.Host) - body.SmtpPort = nullable.NewNullableWithValue(strconv.Itoa(int(s.Port))) - body.SmtpUser = nullable.NewNullableWithValue(s.User) - if len(s.Pass.SHA256) > 0 { - body.SmtpPass = nullable.NewNullableWithValue(s.Pass.Value) - } - body.SmtpAdminEmail = nullable.NewNullableWithValue(s.AdminEmail) - body.SmtpSenderName = nullable.NewNullableWithValue(s.SenderName) -} - -func (s *smtp) fromAuthConfig(remoteConfig v1API.AuthConfigResponseOutput) { - // When local config is not set, we assume platform defaults should not change - if s == nil { - return - } - if s.Enabled { - s.Host = ValOrDefault(remoteConfig.SmtpHost, "") - s.User = ValOrDefault(remoteConfig.SmtpUser, "") - if len(s.Pass.SHA256) > 0 { - s.Pass.SHA256 = ValOrDefault(remoteConfig.SmtpPass, "") - } - s.AdminEmail = ValOrDefault(remoteConfig.SmtpAdminEmail, openapi_types.Email("")) - s.SenderName = ValOrDefault(remoteConfig.SmtpSenderName, "") - portStr := ValOrDefault(remoteConfig.SmtpPort, "0") - if port, err := strconv.ParseUint(portStr, 10, 16); err == nil { - s.Port = uint16(port) - } - } - // Api resets all values when SMTP is disabled - s.Enabled = remoteConfig.SmtpHost != nil -} - -func (s sms) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) { - body.ExternalPhoneEnabled = nullable.NewNullableWithValue(s.EnableSignup) - body.SmsMaxFrequency = nullable.NewNullableWithValue(int(s.MaxFrequency.Seconds())) - body.SmsAutoconfirm = nullable.NewNullableWithValue(s.EnableConfirmations) - body.SmsTemplate = nullable.NewNullableWithValue(s.Template) - if otpString := mapToEnv(s.TestOTP); len(otpString) > 0 { - body.SmsTestOtp = nullable.NewNullableWithValue(otpString) - // Set a 10 year validity for test OTP - timestamp := time.Now().UTC().AddDate(10, 0, 0) - body.SmsTestOtpValidUntil = nullable.NewNullableWithValue(timestamp) - } - // Api only overrides configs of enabled providers - switch { - case s.Twilio.Enabled: - body.SmsProvider = nullable.NewNullableWithValue(v1API.Twilio) - if len(s.Twilio.AuthToken.SHA256) > 0 { - body.SmsTwilioAuthToken = nullable.NewNullableWithValue(s.Twilio.AuthToken.Value) - } - body.SmsTwilioAccountSid = nullable.NewNullableWithValue(s.Twilio.AccountSid) - body.SmsTwilioMessageServiceSid = nullable.NewNullableWithValue(s.Twilio.MessageServiceSid) - case s.TwilioVerify.Enabled: - body.SmsProvider = nullable.NewNullableWithValue(v1API.TwilioVerify) - if len(s.TwilioVerify.AuthToken.SHA256) > 0 { - body.SmsTwilioVerifyAuthToken = nullable.NewNullableWithValue(s.TwilioVerify.AuthToken.Value) - } - body.SmsTwilioVerifyAccountSid = nullable.NewNullableWithValue(s.TwilioVerify.AccountSid) - body.SmsTwilioVerifyMessageServiceSid = nullable.NewNullableWithValue(s.TwilioVerify.MessageServiceSid) - case s.Messagebird.Enabled: - body.SmsProvider = nullable.NewNullableWithValue(v1API.Messagebird) - if len(s.Messagebird.AccessKey.SHA256) > 0 { - body.SmsMessagebirdAccessKey = nullable.NewNullableWithValue(s.Messagebird.AccessKey.Value) - } - body.SmsMessagebirdOriginator = nullable.NewNullableWithValue(s.Messagebird.Originator) - case s.Textlocal.Enabled: - body.SmsProvider = nullable.NewNullableWithValue(v1API.Textlocal) - if len(s.Textlocal.ApiKey.SHA256) > 0 { - body.SmsTextlocalApiKey = nullable.NewNullableWithValue(s.Textlocal.ApiKey.Value) - } - body.SmsTextlocalSender = nullable.NewNullableWithValue(s.Textlocal.Sender) - case s.Vonage.Enabled: - body.SmsProvider = nullable.NewNullableWithValue(v1API.Vonage) - if len(s.Vonage.ApiSecret.SHA256) > 0 { - body.SmsVonageApiSecret = nullable.NewNullableWithValue(s.Vonage.ApiSecret.Value) - } - body.SmsVonageApiKey = nullable.NewNullableWithValue(s.Vonage.ApiKey) - body.SmsVonageFrom = nullable.NewNullableWithValue(s.Vonage.From) - } -} - -func (s *sms) fromAuthConfig(remoteConfig v1API.AuthConfigResponseOutput) { - s.EnableSignup = ValOrDefault(remoteConfig.ExternalPhoneEnabled, false) - s.MaxFrequency = time.Duration(ValOrDefault(remoteConfig.SmsMaxFrequency, 0)) * time.Second - s.EnableConfirmations = ValOrDefault(remoteConfig.SmsAutoconfirm, false) - s.Template = ValOrDefault(remoteConfig.SmsTemplate, "") - s.TestOTP = envToMap(ValOrDefault(remoteConfig.SmsTestOtp, "")) - // We are only interested in the provider that's enabled locally - switch { - case s.Twilio.Enabled: - if len(s.Twilio.AuthToken.SHA256) > 0 { - s.Twilio.AuthToken.SHA256 = ValOrDefault(remoteConfig.SmsTwilioAuthToken, "") - } - s.Twilio.AccountSid = ValOrDefault(remoteConfig.SmsTwilioAccountSid, "") - s.Twilio.MessageServiceSid = ValOrDefault(remoteConfig.SmsTwilioMessageServiceSid, "") - case s.TwilioVerify.Enabled: - if len(s.TwilioVerify.AuthToken.SHA256) > 0 { - s.TwilioVerify.AuthToken.SHA256 = ValOrDefault(remoteConfig.SmsTwilioVerifyAuthToken, "") - } - s.TwilioVerify.AccountSid = ValOrDefault(remoteConfig.SmsTwilioVerifyAccountSid, "") - s.TwilioVerify.MessageServiceSid = ValOrDefault(remoteConfig.SmsTwilioVerifyMessageServiceSid, "") - case s.Messagebird.Enabled: - if len(s.Messagebird.AccessKey.SHA256) > 0 { - s.Messagebird.AccessKey.SHA256 = ValOrDefault(remoteConfig.SmsMessagebirdAccessKey, "") - } - s.Messagebird.Originator = ValOrDefault(remoteConfig.SmsMessagebirdOriginator, "") - case s.Textlocal.Enabled: - if len(s.Textlocal.ApiKey.SHA256) > 0 { - s.Textlocal.ApiKey.SHA256 = ValOrDefault(remoteConfig.SmsTextlocalApiKey, "") - } - s.Textlocal.Sender = ValOrDefault(remoteConfig.SmsTextlocalSender, "") - case s.Vonage.Enabled: - if len(s.Vonage.ApiSecret.SHA256) > 0 { - s.Vonage.ApiSecret.SHA256 = ValOrDefault(remoteConfig.SmsVonageApiSecret, "") - } - s.Vonage.ApiKey = ValOrDefault(remoteConfig.SmsVonageApiKey, "") - s.Vonage.From = ValOrDefault(remoteConfig.SmsVonageFrom, "") - case !s.EnableSignup: - // Nothing to do if both local and remote providers are disabled. - return - } - if provider := ValOrDefault(remoteConfig.SmsProvider, ""); len(provider) > 0 { - s.Twilio.Enabled = provider == "twilio" - s.TwilioVerify.Enabled = provider == "twilio_verify" - s.Messagebird.Enabled = provider == "messagebird" - s.Textlocal.Enabled = provider == "textlocal" - s.Vonage.Enabled = provider == "vonage" - } -} - -func (e external) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) { - if len(e) == 0 { - return - } - // Ignore configs of disabled providers because their envs are not loaded - if p, ok := e["apple"]; ok { - if body.ExternalAppleEnabled = nullable.NewNullableWithValue(p.Enabled); p.Enabled { - body.ExternalAppleClientId = nullable.NewNullableWithValue(p.ClientId) - if len(p.Secret.SHA256) > 0 { - body.ExternalAppleSecret = nullable.NewNullableWithValue(p.Secret.Value) - } - body.ExternalAppleEmailOptional = nullable.NewNullableWithValue(p.EmailOptional) - } - } - if p, ok := e["azure"]; ok { - if body.ExternalAzureEnabled = nullable.NewNullableWithValue(p.Enabled); p.Enabled { - body.ExternalAzureClientId = nullable.NewNullableWithValue(p.ClientId) - if len(p.Secret.SHA256) > 0 { - body.ExternalAzureSecret = nullable.NewNullableWithValue(p.Secret.Value) - } - body.ExternalAzureUrl = nullable.NewNullableWithValue(p.Url) - body.ExternalAzureEmailOptional = nullable.NewNullableWithValue(p.EmailOptional) - } - } - if p, ok := e["bitbucket"]; ok { - if body.ExternalBitbucketEnabled = nullable.NewNullableWithValue(p.Enabled); p.Enabled { - body.ExternalBitbucketClientId = nullable.NewNullableWithValue(p.ClientId) - if len(p.Secret.SHA256) > 0 { - body.ExternalBitbucketSecret = nullable.NewNullableWithValue(p.Secret.Value) - } - body.ExternalBitbucketEmailOptional = nullable.NewNullableWithValue(p.EmailOptional) - } - } - if p, ok := e["discord"]; ok { - if body.ExternalDiscordEnabled = nullable.NewNullableWithValue(p.Enabled); p.Enabled { - body.ExternalDiscordClientId = nullable.NewNullableWithValue(p.ClientId) - if len(p.Secret.SHA256) > 0 { - body.ExternalDiscordSecret = nullable.NewNullableWithValue(p.Secret.Value) - } - body.ExternalDiscordEmailOptional = nullable.NewNullableWithValue(p.EmailOptional) - } - } - if p, ok := e["facebook"]; ok { - if body.ExternalFacebookEnabled = nullable.NewNullableWithValue(p.Enabled); p.Enabled { - body.ExternalFacebookClientId = nullable.NewNullableWithValue(p.ClientId) - if len(p.Secret.SHA256) > 0 { - body.ExternalFacebookSecret = nullable.NewNullableWithValue(p.Secret.Value) - } - body.ExternalFacebookEmailOptional = nullable.NewNullableWithValue(p.EmailOptional) - } - } - if p, ok := e["figma"]; ok { - if body.ExternalFigmaEnabled = nullable.NewNullableWithValue(p.Enabled); p.Enabled { - body.ExternalFigmaClientId = nullable.NewNullableWithValue(p.ClientId) - if len(p.Secret.SHA256) > 0 { - body.ExternalFigmaSecret = nullable.NewNullableWithValue(p.Secret.Value) - } - body.ExternalFigmaEmailOptional = nullable.NewNullableWithValue(p.EmailOptional) - } - } - if p, ok := e["github"]; ok { - if body.ExternalGithubEnabled = nullable.NewNullableWithValue(p.Enabled); p.Enabled { - body.ExternalGithubClientId = nullable.NewNullableWithValue(p.ClientId) - if len(p.Secret.SHA256) > 0 { - body.ExternalGithubSecret = nullable.NewNullableWithValue(p.Secret.Value) - } - body.ExternalGithubEmailOptional = nullable.NewNullableWithValue(p.EmailOptional) - } - } - if p, ok := e["gitlab"]; ok { - if body.ExternalGitlabEnabled = nullable.NewNullableWithValue(p.Enabled); p.Enabled { - body.ExternalGitlabClientId = nullable.NewNullableWithValue(p.ClientId) - if len(p.Secret.SHA256) > 0 { - body.ExternalGitlabSecret = nullable.NewNullableWithValue(p.Secret.Value) - } - body.ExternalGitlabUrl = nullable.NewNullableWithValue(p.Url) - body.ExternalGitlabEmailOptional = nullable.NewNullableWithValue(p.EmailOptional) - } - } - if p, ok := e["google"]; ok { - if body.ExternalGoogleEnabled = nullable.NewNullableWithValue(p.Enabled); p.Enabled { - body.ExternalGoogleClientId = nullable.NewNullableWithValue(p.ClientId) - if len(p.Secret.SHA256) > 0 { - body.ExternalGoogleSecret = nullable.NewNullableWithValue(p.Secret.Value) - } - body.ExternalGoogleSkipNonceCheck = nullable.NewNullableWithValue(p.SkipNonceCheck) - body.ExternalGoogleEmailOptional = nullable.NewNullableWithValue(p.EmailOptional) - } - } - if p, ok := e["kakao"]; ok { - if body.ExternalKakaoEnabled = nullable.NewNullableWithValue(p.Enabled); p.Enabled { - body.ExternalKakaoClientId = nullable.NewNullableWithValue(p.ClientId) - if len(p.Secret.SHA256) > 0 { - body.ExternalKakaoSecret = nullable.NewNullableWithValue(p.Secret.Value) - } - body.ExternalKakaoEmailOptional = nullable.NewNullableWithValue(p.EmailOptional) - } - } - if p, ok := e["keycloak"]; ok { - if body.ExternalKeycloakEnabled = nullable.NewNullableWithValue(p.Enabled); p.Enabled { - body.ExternalKeycloakClientId = nullable.NewNullableWithValue(p.ClientId) - if len(p.Secret.SHA256) > 0 { - body.ExternalKeycloakSecret = nullable.NewNullableWithValue(p.Secret.Value) - } - body.ExternalKeycloakUrl = nullable.NewNullableWithValue(p.Url) - body.ExternalKeycloakEmailOptional = nullable.NewNullableWithValue(p.EmailOptional) - } - } - if p, ok := e["linkedin_oidc"]; ok { - if body.ExternalLinkedinOidcEnabled = nullable.NewNullableWithValue(p.Enabled); p.Enabled { - body.ExternalLinkedinOidcClientId = nullable.NewNullableWithValue(p.ClientId) - if len(p.Secret.SHA256) > 0 { - body.ExternalLinkedinOidcSecret = nullable.NewNullableWithValue(p.Secret.Value) - } - body.ExternalLinkedinOidcEmailOptional = nullable.NewNullableWithValue(p.EmailOptional) - } - } - if p, ok := e["notion"]; ok { - if body.ExternalNotionEnabled = nullable.NewNullableWithValue(p.Enabled); p.Enabled { - body.ExternalNotionClientId = nullable.NewNullableWithValue(p.ClientId) - if len(p.Secret.SHA256) > 0 { - body.ExternalNotionSecret = nullable.NewNullableWithValue(p.Secret.Value) - } - body.ExternalNotionEmailOptional = nullable.NewNullableWithValue(p.EmailOptional) - } - } - if p, ok := e["slack_oidc"]; ok { - if body.ExternalSlackOidcEnabled = nullable.NewNullableWithValue(p.Enabled); p.Enabled { - body.ExternalSlackOidcClientId = nullable.NewNullableWithValue(p.ClientId) - if len(p.Secret.SHA256) > 0 { - body.ExternalSlackOidcSecret = nullable.NewNullableWithValue(p.Secret.Value) - } - body.ExternalSlackOidcEmailOptional = nullable.NewNullableWithValue(p.EmailOptional) - } - } - if p, ok := e["spotify"]; ok { - if body.ExternalSpotifyEnabled = nullable.NewNullableWithValue(p.Enabled); p.Enabled { - body.ExternalSpotifyClientId = nullable.NewNullableWithValue(p.ClientId) - if len(p.Secret.SHA256) > 0 { - body.ExternalSpotifySecret = nullable.NewNullableWithValue(p.Secret.Value) - } - body.ExternalSpotifyEmailOptional = nullable.NewNullableWithValue(p.EmailOptional) - } - } - if p, ok := e["twitch"]; ok { - if body.ExternalTwitchEnabled = nullable.NewNullableWithValue(p.Enabled); p.Enabled { - body.ExternalTwitchClientId = nullable.NewNullableWithValue(p.ClientId) - if len(p.Secret.SHA256) > 0 { - body.ExternalTwitchSecret = nullable.NewNullableWithValue(p.Secret.Value) - } - body.ExternalTwitchEmailOptional = nullable.NewNullableWithValue(p.EmailOptional) - } - } - if p, ok := e["twitter"]; ok { - if body.ExternalTwitterEnabled = nullable.NewNullableWithValue(p.Enabled); p.Enabled { - body.ExternalTwitterClientId = nullable.NewNullableWithValue(p.ClientId) - if len(p.Secret.SHA256) > 0 { - body.ExternalTwitterSecret = nullable.NewNullableWithValue(p.Secret.Value) - } - body.ExternalTwitterEmailOptional = nullable.NewNullableWithValue(p.EmailOptional) - } - } - if p, ok := e["x"]; ok { - if body.ExternalXEnabled = nullable.NewNullableWithValue(p.Enabled); p.Enabled { - body.ExternalXClientId = nullable.NewNullableWithValue(p.ClientId) - if len(p.Secret.SHA256) > 0 { - body.ExternalXSecret = nullable.NewNullableWithValue(p.Secret.Value) - } - body.ExternalXEmailOptional = nullable.NewNullableWithValue(p.EmailOptional) - } - } - if p, ok := e["workos"]; ok { - if body.ExternalWorkosEnabled = nullable.NewNullableWithValue(p.Enabled); p.Enabled { - body.ExternalWorkosClientId = nullable.NewNullableWithValue(p.ClientId) - if len(p.Secret.SHA256) > 0 { - body.ExternalWorkosSecret = nullable.NewNullableWithValue(p.Secret.Value) - } - body.ExternalWorkosUrl = nullable.NewNullableWithValue(p.Url) - } - } - if p, ok := e["zoom"]; ok { - if body.ExternalZoomEnabled = nullable.NewNullableWithValue(p.Enabled); p.Enabled { - body.ExternalZoomClientId = nullable.NewNullableWithValue(p.ClientId) - if len(p.Secret.SHA256) > 0 { - body.ExternalZoomSecret = nullable.NewNullableWithValue(p.Secret.Value) - } - body.ExternalZoomEmailOptional = nullable.NewNullableWithValue(p.EmailOptional) - } - } -} - -func (e external) fromAuthConfig(remoteConfig v1API.AuthConfigResponseOutput) { - if len(e) == 0 { - return - } - // Ignore configs of disabled providers because their envs are not loaded - if p, ok := e["apple"]; ok { - if p.Enabled { - p.ClientId = ValOrDefault(remoteConfig.ExternalAppleClientId, "") - if ids := ValOrDefault(remoteConfig.ExternalAppleAdditionalClientIds, ""); len(ids) > 0 { - p.ClientId += "," + ids - } - if len(p.Secret.SHA256) > 0 { - p.Secret.SHA256 = ValOrDefault(remoteConfig.ExternalAppleSecret, "") - } - p.EmailOptional = ValOrDefault(remoteConfig.ExternalAppleEmailOptional, false) - } - p.Enabled = ValOrDefault(remoteConfig.ExternalAppleEnabled, false) - e["apple"] = p - } - - if p, ok := e["azure"]; ok { - if p.Enabled { - p.ClientId = ValOrDefault(remoteConfig.ExternalAzureClientId, "") - if len(p.Secret.SHA256) > 0 { - p.Secret.SHA256 = ValOrDefault(remoteConfig.ExternalAzureSecret, "") - } - p.Url = ValOrDefault(remoteConfig.ExternalAzureUrl, "") - p.EmailOptional = ValOrDefault(remoteConfig.ExternalAzureEmailOptional, false) - } - p.Enabled = ValOrDefault(remoteConfig.ExternalAzureEnabled, false) - e["azure"] = p - } - - if p, ok := e["bitbucket"]; ok { - if p.Enabled { - p.ClientId = ValOrDefault(remoteConfig.ExternalBitbucketClientId, "") - if len(p.Secret.SHA256) > 0 { - p.Secret.SHA256 = ValOrDefault(remoteConfig.ExternalBitbucketSecret, "") - } - p.EmailOptional = ValOrDefault(remoteConfig.ExternalBitbucketEmailOptional, false) - } - p.Enabled = ValOrDefault(remoteConfig.ExternalBitbucketEnabled, false) - e["bitbucket"] = p - } - - if p, ok := e["discord"]; ok { - if p.Enabled { - p.ClientId = ValOrDefault(remoteConfig.ExternalDiscordClientId, "") - if len(p.Secret.SHA256) > 0 { - p.Secret.SHA256 = ValOrDefault(remoteConfig.ExternalDiscordSecret, "") - } - p.EmailOptional = ValOrDefault(remoteConfig.ExternalDiscordEmailOptional, false) - } - p.Enabled = ValOrDefault(remoteConfig.ExternalDiscordEnabled, false) - e["discord"] = p - } - - if p, ok := e["facebook"]; ok { - if p.Enabled { - p.ClientId = ValOrDefault(remoteConfig.ExternalFacebookClientId, "") - if len(p.Secret.SHA256) > 0 { - p.Secret.SHA256 = ValOrDefault(remoteConfig.ExternalFacebookSecret, "") - } - p.EmailOptional = ValOrDefault(remoteConfig.ExternalFacebookEmailOptional, false) - } - p.Enabled = ValOrDefault(remoteConfig.ExternalFacebookEnabled, false) - e["facebook"] = p - } - - if p, ok := e["figma"]; ok { - if p.Enabled { - p.ClientId = ValOrDefault(remoteConfig.ExternalFigmaClientId, "") - if len(p.Secret.SHA256) > 0 { - p.Secret.SHA256 = ValOrDefault(remoteConfig.ExternalFigmaSecret, "") - } - p.EmailOptional = ValOrDefault(remoteConfig.ExternalFigmaEmailOptional, false) - } - p.Enabled = ValOrDefault(remoteConfig.ExternalFigmaEnabled, false) - e["figma"] = p - } - - if p, ok := e["github"]; ok { - if p.Enabled { - p.ClientId = ValOrDefault(remoteConfig.ExternalGithubClientId, "") - if len(p.Secret.SHA256) > 0 { - p.Secret.SHA256 = ValOrDefault(remoteConfig.ExternalGithubSecret, "") - } - p.EmailOptional = ValOrDefault(remoteConfig.ExternalGithubEmailOptional, false) - } - p.Enabled = ValOrDefault(remoteConfig.ExternalGithubEnabled, false) - e["github"] = p - } - - if p, ok := e["gitlab"]; ok { - if p.Enabled { - p.ClientId = ValOrDefault(remoteConfig.ExternalGitlabClientId, "") - if len(p.Secret.SHA256) > 0 { - p.Secret.SHA256 = ValOrDefault(remoteConfig.ExternalGitlabSecret, "") - } - p.Url = ValOrDefault(remoteConfig.ExternalGitlabUrl, "") - p.EmailOptional = ValOrDefault(remoteConfig.ExternalGitlabEmailOptional, false) - } - p.Enabled = ValOrDefault(remoteConfig.ExternalGitlabEnabled, false) - e["gitlab"] = p - } - - if p, ok := e["google"]; ok { - if p.Enabled { - p.ClientId = ValOrDefault(remoteConfig.ExternalGoogleClientId, "") - if ids := ValOrDefault(remoteConfig.ExternalGoogleAdditionalClientIds, ""); len(ids) > 0 { - p.ClientId += "," + ids - } - if len(p.Secret.SHA256) > 0 { - p.Secret.SHA256 = ValOrDefault(remoteConfig.ExternalGoogleSecret, "") - } - p.SkipNonceCheck = ValOrDefault(remoteConfig.ExternalGoogleSkipNonceCheck, false) - p.EmailOptional = ValOrDefault(remoteConfig.ExternalGoogleEmailOptional, false) - } - p.Enabled = ValOrDefault(remoteConfig.ExternalGoogleEnabled, false) - e["google"] = p - } - - if p, ok := e["kakao"]; ok { - if p.Enabled { - p.ClientId = ValOrDefault(remoteConfig.ExternalKakaoClientId, "") - if len(p.Secret.SHA256) > 0 { - p.Secret.SHA256 = ValOrDefault(remoteConfig.ExternalKakaoSecret, "") - } - p.EmailOptional = ValOrDefault(remoteConfig.ExternalKakaoEmailOptional, false) - } - p.Enabled = ValOrDefault(remoteConfig.ExternalKakaoEnabled, false) - e["kakao"] = p - } - - if p, ok := e["keycloak"]; ok { - if p.Enabled { - p.ClientId = ValOrDefault(remoteConfig.ExternalKeycloakClientId, "") - if len(p.Secret.SHA256) > 0 { - p.Secret.SHA256 = ValOrDefault(remoteConfig.ExternalKeycloakSecret, "") - } - p.Url = ValOrDefault(remoteConfig.ExternalKeycloakUrl, "") - p.EmailOptional = ValOrDefault(remoteConfig.ExternalKeycloakEmailOptional, false) - } - p.Enabled = ValOrDefault(remoteConfig.ExternalKeycloakEnabled, false) - e["keycloak"] = p - } - - if p, ok := e["linkedin_oidc"]; ok { - if p.Enabled { - p.ClientId = ValOrDefault(remoteConfig.ExternalLinkedinOidcClientId, "") - if len(p.Secret.SHA256) > 0 { - p.Secret.SHA256 = ValOrDefault(remoteConfig.ExternalLinkedinOidcSecret, "") - } - p.EmailOptional = ValOrDefault(remoteConfig.ExternalLinkedinOidcEmailOptional, false) - } - p.Enabled = ValOrDefault(remoteConfig.ExternalLinkedinOidcEnabled, false) - e["linkedin_oidc"] = p - } - - if p, ok := e["notion"]; ok { - if p.Enabled { - p.ClientId = ValOrDefault(remoteConfig.ExternalNotionClientId, "") - if len(p.Secret.SHA256) > 0 { - p.Secret.SHA256 = ValOrDefault(remoteConfig.ExternalNotionSecret, "") - } - p.EmailOptional = ValOrDefault(remoteConfig.ExternalNotionEmailOptional, false) - } - p.Enabled = ValOrDefault(remoteConfig.ExternalNotionEnabled, false) - e["notion"] = p - } - - if p, ok := e["slack_oidc"]; ok { - if p.Enabled { - p.ClientId = ValOrDefault(remoteConfig.ExternalSlackOidcClientId, "") - if len(p.Secret.SHA256) > 0 { - p.Secret.SHA256 = ValOrDefault(remoteConfig.ExternalSlackOidcSecret, "") - } - p.EmailOptional = ValOrDefault(remoteConfig.ExternalSlackOidcEmailOptional, false) - } - p.Enabled = ValOrDefault(remoteConfig.ExternalSlackOidcEnabled, false) - e["slack_oidc"] = p - } - - if p, ok := e["spotify"]; ok { - if p.Enabled { - p.ClientId = ValOrDefault(remoteConfig.ExternalSpotifyClientId, "") - if len(p.Secret.SHA256) > 0 { - p.Secret.SHA256 = ValOrDefault(remoteConfig.ExternalSpotifySecret, "") - } - p.EmailOptional = ValOrDefault(remoteConfig.ExternalSpotifyEmailOptional, false) - } - p.Enabled = ValOrDefault(remoteConfig.ExternalSpotifyEnabled, false) - e["spotify"] = p - } - - if p, ok := e["twitch"]; ok { - if p.Enabled { - p.ClientId = ValOrDefault(remoteConfig.ExternalTwitchClientId, "") - if len(p.Secret.SHA256) > 0 { - p.Secret.SHA256 = ValOrDefault(remoteConfig.ExternalTwitchSecret, "") - } - p.EmailOptional = ValOrDefault(remoteConfig.ExternalTwitchEmailOptional, false) - } - p.Enabled = ValOrDefault(remoteConfig.ExternalTwitchEnabled, false) - e["twitch"] = p - } - - if p, ok := e["twitter"]; ok { - if p.Enabled { - p.ClientId = ValOrDefault(remoteConfig.ExternalTwitterClientId, "") - if len(p.Secret.SHA256) > 0 { - p.Secret.SHA256 = ValOrDefault(remoteConfig.ExternalTwitterSecret, "") - } - p.EmailOptional = ValOrDefault(remoteConfig.ExternalTwitterEmailOptional, false) - } - p.Enabled = ValOrDefault(remoteConfig.ExternalTwitterEnabled, false) - e["twitter"] = p - } - - if p, ok := e["x"]; ok { - if p.Enabled { - p.ClientId = ValOrDefault(remoteConfig.ExternalXClientId, "") - if len(p.Secret.SHA256) > 0 { - p.Secret.SHA256 = ValOrDefault(remoteConfig.ExternalXSecret, "") - } - p.EmailOptional = ValOrDefault(remoteConfig.ExternalXEmailOptional, false) - } - p.Enabled = ValOrDefault(remoteConfig.ExternalXEnabled, false) - e["x"] = p - } - - if p, ok := e["workos"]; ok { - if p.Enabled { - p.ClientId = ValOrDefault(remoteConfig.ExternalWorkosClientId, "") - if len(p.Secret.SHA256) > 0 { - p.Secret.SHA256 = ValOrDefault(remoteConfig.ExternalWorkosSecret, "") - } - p.Url = ValOrDefault(remoteConfig.ExternalWorkosUrl, "") - } - p.Enabled = ValOrDefault(remoteConfig.ExternalWorkosEnabled, false) - e["workos"] = p - } - - if p, ok := e["zoom"]; ok { - if p.Enabled { - p.ClientId = ValOrDefault(remoteConfig.ExternalZoomClientId, "") - if len(p.Secret.SHA256) > 0 { - p.Secret.SHA256 = ValOrDefault(remoteConfig.ExternalZoomSecret, "") - } - p.EmailOptional = ValOrDefault(remoteConfig.ExternalZoomEmailOptional, false) - } - p.Enabled = ValOrDefault(remoteConfig.ExternalZoomEnabled, false) - e["zoom"] = p - } -} - -func (w web3) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) { - body.ExternalWeb3SolanaEnabled = nullable.NewNullableWithValue(w.Solana.Enabled) - body.ExternalWeb3EthereumEnabled = nullable.NewNullableWithValue(w.Ethereum.Enabled) -} - -func (w *web3) fromAuthConfig(remoteConfig v1API.AuthConfigResponseOutput) { - if value, err := remoteConfig.ExternalWeb3SolanaEnabled.Get(); err == nil { - w.Solana.Enabled = value - } - - if value, err := remoteConfig.ExternalWeb3EthereumEnabled.Get(); err == nil { - w.Ethereum.Enabled = value - } -} - -func (o OAuthServer) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) { - // TODO(cemal) :: implement me - // OAuth server configuration is behind a feature flag in the remote API - // Will be implemented when the feature reaches GA -} - -func (o *OAuthServer) fromAuthConfig(remoteConfig v1API.AuthConfigResponseOutput) { - // TODO(cemal) :: implement me - // OAuth server configuration is behind a feature flag in the remote API - // Will be implemented when the feature reaches GA -} - -func (a *auth) DiffWithRemote(remoteConfig v1API.AuthConfigResponseOutput, filter ...func(string) bool) ([]byte, error) { - copy := a.Clone() - copy.FromRemoteAuthConfig(remoteConfig) - // Confirm cost before enabling addons - for _, keep := range filter { - if a.MFA.Phone.VerifyEnabled && !copy.MFA.Phone.VerifyEnabled { - if !keep(string(v1API.ListProjectAddonsResponseOutputAvailableAddonsTypeAuthMfaPhone)) { - a.MFA.Phone.VerifyEnabled = false - // Enroll cannot be enabled on its own - a.MFA.Phone.EnrollEnabled = false - } - } - if a.MFA.WebAuthn.VerifyEnabled && !copy.MFA.WebAuthn.VerifyEnabled { - if !keep(string(v1API.ListProjectAddonsResponseOutputAvailableAddonsTypeAuthMfaWebAuthn)) { - a.MFA.WebAuthn.VerifyEnabled = false - // Enroll cannot be enabled on its own - a.MFA.WebAuthn.EnrollEnabled = false - } - } - } - // Convert the config values into easily comparable remoteConfig values - currentValue, err := ToTomlBytes(a) - if err != nil { - return nil, err - } - remoteCompare, err := ToTomlBytes(copy) - if err != nil { - return nil, err - } - return diff.Diff("remote[auth]", remoteCompare, "local[auth]", currentValue), nil -} - -func ValOrDefault[T any](v nullable.Nullable[T], def T) T { - if value, err := v.Get(); err == nil { - return value - } - return def -} diff --git a/apps/cli-go/pkg/config/auth_test.go b/apps/cli-go/pkg/config/auth_test.go deleted file mode 100644 index 50772a5571..0000000000 --- a/apps/cli-go/pkg/config/auth_test.go +++ /dev/null @@ -1,1497 +0,0 @@ -package config - -import ( - "os" - "path/filepath" - "testing" - "time" - - "github.com/go-errors/errors" - "github.com/oapi-codegen/nullable" - openapi_types "github.com/oapi-codegen/runtime/types" - "github.com/stretchr/testify/assert" - v1API "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/cast" -) - -func newWithDefaults() auth { - return auth{ - EnableSignup: true, - AdditionalRedirectUrls: []string{}, - Email: email{ - EnableConfirmations: true, - }, - Sms: sms{ - TestOTP: map[string]string{}, - }, - } -} - -func assertSnapshotEqual(t *testing.T, actual []byte) { - snapshot := filepath.Join("testdata", filepath.FromSlash(t.Name())) + ".diff" - expected, err := os.ReadFile(snapshot) - if errors.Is(err, os.ErrNotExist) { - assert.NoError(t, os.MkdirAll(filepath.Dir(snapshot), 0755)) - assert.NoError(t, os.WriteFile(snapshot, actual, 0600)) - } - assert.Equal(t, string(expected), string(actual)) -} - -func TestAuthDiff(t *testing.T) { - t.Run("local and remote enabled", func(t *testing.T) { - c := newWithDefaults() - c.SiteUrl = "http://127.0.0.1:3000" - c.AdditionalRedirectUrls = []string{"https://127.0.0.1:3000"} - c.JwtExpiry = 3600 - c.EnableRefreshTokenRotation = true - c.RefreshTokenReuseInterval = 10 - c.EnableManualLinking = true - c.EnableSignup = true - c.EnableAnonymousSignIns = true - c.MinimumPasswordLength = 6 - c.PasswordRequirements = LettersDigits - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - SiteUrl: nullable.NewNullableWithValue("http://127.0.0.1:3000"), - UriAllowList: nullable.NewNullableWithValue("https://127.0.0.1:3000"), - JwtExp: nullable.NewNullableWithValue(3600), - RefreshTokenRotationEnabled: nullable.NewNullableWithValue(true), - SecurityRefreshTokenReuseInterval: nullable.NewNullableWithValue(10), - SecurityManualLinkingEnabled: nullable.NewNullableWithValue(true), - DisableSignup: nullable.NewNullableWithValue(false), - ExternalAnonymousUsersEnabled: nullable.NewNullableWithValue(true), - PasswordMinLength: nullable.NewNullableWithValue(6), - PasswordRequiredCharacters: nullable.NewNullableWithValue(v1API.AuthConfigResponseOutputPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789), - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, string(diff)) - }) - - t.Run("local enabled and disabled", func(t *testing.T) { - c := newWithDefaults() - c.SiteUrl = "http://127.0.0.1:3000" - c.AdditionalRedirectUrls = []string{"https://127.0.0.1:3000"} - c.JwtExpiry = 3600 - c.EnableRefreshTokenRotation = false - c.RefreshTokenReuseInterval = 10 - c.EnableManualLinking = false - c.EnableSignup = false - c.EnableAnonymousSignIns = false - c.MinimumPasswordLength = 6 - c.PasswordRequirements = LowerUpperLettersDigitsSymbols - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - SiteUrl: nullable.NewNullableWithValue(""), - UriAllowList: nullable.NewNullableWithValue("https://127.0.0.1:3000,https://ref.supabase.co"), - JwtExp: nullable.NewNullableWithValue(0), - RefreshTokenRotationEnabled: nullable.NewNullableWithValue(true), - SecurityRefreshTokenReuseInterval: nullable.NewNullableWithValue(0), - SecurityManualLinkingEnabled: nullable.NewNullableWithValue(true), - DisableSignup: nullable.NewNullableWithValue(false), - ExternalAnonymousUsersEnabled: nullable.NewNullableWithValue(true), - PasswordMinLength: nullable.NewNullableWithValue(8), - PasswordRequiredCharacters: nullable.NewNullableWithValue(v1API.AuthConfigResponseOutputPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789), - }) - // Check error - assert.NoError(t, err) - assertSnapshotEqual(t, diff) - }) - - t.Run("local and remote disabled", func(t *testing.T) { - c := newWithDefaults() - c.EnableSignup = false - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - SiteUrl: nullable.NewNullableWithValue(""), - UriAllowList: nullable.NewNullableWithValue(""), - JwtExp: nullable.NewNullableWithValue(0), - RefreshTokenRotationEnabled: nullable.NewNullableWithValue(false), - SecurityRefreshTokenReuseInterval: nullable.NewNullableWithValue(0), - SecurityManualLinkingEnabled: nullable.NewNullableWithValue(false), - DisableSignup: nullable.NewNullableWithValue(true), - ExternalAnonymousUsersEnabled: nullable.NewNullableWithValue(false), - PasswordMinLength: nullable.NewNullableWithValue(0), - PasswordRequiredCharacters: nullable.NewNullableWithValue(v1API.AuthConfigResponseOutputPasswordRequiredCharactersEmpty), - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, string(diff)) - }) -} - -func TestCaptchaDiff(t *testing.T) { - t.Run("local and remote enabled", func(t *testing.T) { - c := newWithDefaults() - c.Captcha = &captcha{ - Enabled: true, - Provider: HCaptchaProvider, - Secret: Secret{ - Value: "test-secret", - SHA256: "ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252", - }, - } - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - SecurityCaptchaEnabled: nullable.NewNullableWithValue(true), - SecurityCaptchaProvider: nullable.NewNullableWithValue(v1API.AuthConfigResponseOutputSecurityCaptchaProviderHcaptcha), - SecurityCaptchaSecret: nullable.NewNullableWithValue("ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"), - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, string(diff)) - }) - - t.Run("local disabled remote enabled", func(t *testing.T) { - c := newWithDefaults() - c.Captcha = &captcha{ - Enabled: false, - Provider: TurnstileProvider, - Secret: Secret{ - Value: "test-key", - SHA256: "ed64b7695a606bc6ab4fcb41fe815b5ddf1063ccbc87afe1fa89756635db520e", - }, - } - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - SecurityCaptchaEnabled: nullable.NewNullableWithValue(true), - SecurityCaptchaProvider: nullable.NewNullableWithValue(v1API.AuthConfigResponseOutputSecurityCaptchaProviderHcaptcha), - SecurityCaptchaSecret: nullable.NewNullableWithValue("ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"), - }) - // Check error - assert.NoError(t, err) - assertSnapshotEqual(t, diff) - }) - - t.Run("local enabled remote disabled", func(t *testing.T) { - c := newWithDefaults() - c.Captcha = &captcha{ - Enabled: true, - Provider: TurnstileProvider, - Secret: Secret{ - Value: "test-key", - SHA256: "ed64b7695a606bc6ab4fcb41fe815b5ddf1063ccbc87afe1fa89756635db520e", - }, - } - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - SecurityCaptchaEnabled: nullable.NewNullableWithValue(false), - SecurityCaptchaProvider: nullable.NewNullableWithValue(v1API.AuthConfigResponseOutputSecurityCaptchaProviderHcaptcha), - SecurityCaptchaSecret: nullable.NewNullableWithValue("ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"), - }) - // Check error - assert.NoError(t, err) - assertSnapshotEqual(t, diff) - }) - - t.Run("local and remote disabled", func(t *testing.T) { - c := newWithDefaults() - c.Captcha = &captcha{ - Enabled: false, - } - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - SecurityCaptchaEnabled: nullable.NewNullableWithValue(false), - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, string(diff)) - }) - - t.Run("ignores undefined config", func(t *testing.T) { - c := newWithDefaults() - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - SecurityCaptchaEnabled: nullable.NewNullableWithValue(true), - SecurityCaptchaProvider: nullable.NewNullableWithValue(v1API.AuthConfigResponseOutputSecurityCaptchaProviderHcaptcha), - SecurityCaptchaSecret: nullable.NewNullableWithValue("ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"), - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, string(diff)) - }) -} - -func TestPasskeyConfigMapping(t *testing.T) { - t.Run("serializes passkey config to update body", func(t *testing.T) { - c := newWithDefaults() - c.Passkey = &Passkey{Enabled: true} - c.Webauthn = &Webauthn{ - RpDisplayName: "Supabase CLI", - RpId: "localhost", - RpOrigins: []string{ - "http://127.0.0.1:3000", - "https://localhost:3000", - }, - } - // Run test - body := c.ToUpdateAuthConfigBody() - // Check result - if assert.NotNil(t, body.PasskeyEnabled) { - assert.True(t, *body.PasskeyEnabled) - } - assert.Equal(t, "Supabase CLI", ValOrDefault(body.WebauthnRpDisplayName, "")) - assert.Equal(t, "localhost", ValOrDefault(body.WebauthnRpId, "")) - assert.Equal(t, "http://127.0.0.1:3000,https://localhost:3000", ValOrDefault(body.WebauthnRpOrigins, "")) - }) - - t.Run("does not serialize rp fields when webauthn is undefined", func(t *testing.T) { - c := newWithDefaults() - c.Passkey = &Passkey{Enabled: false} - // Run test - body := c.ToUpdateAuthConfigBody() - // Check result - if assert.NotNil(t, body.PasskeyEnabled) { - assert.False(t, *body.PasskeyEnabled) - } - _, err := body.WebauthnRpDisplayName.Get() - assert.Error(t, err) - _, err = body.WebauthnRpId.Get() - assert.Error(t, err) - _, err = body.WebauthnRpOrigins.Get() - assert.Error(t, err) - }) - - t.Run("serializes webauthn fields independently of passkey", func(t *testing.T) { - c := newWithDefaults() - c.Webauthn = &Webauthn{ - RpDisplayName: "Supabase CLI", - RpId: "localhost", - RpOrigins: []string{"http://127.0.0.1:3000"}, - } - // Run test - body := c.ToUpdateAuthConfigBody() - // Check result - assert.Nil(t, body.PasskeyEnabled) - assert.Equal(t, "Supabase CLI", ValOrDefault(body.WebauthnRpDisplayName, "")) - assert.Equal(t, "localhost", ValOrDefault(body.WebauthnRpId, "")) - assert.Equal(t, "http://127.0.0.1:3000", ValOrDefault(body.WebauthnRpOrigins, "")) - }) - - t.Run("hydrates passkey and webauthn config from remote", func(t *testing.T) { - c := newWithDefaults() - c.Passkey = &Passkey{Enabled: true} - c.Webauthn = &Webauthn{} - // Run test - c.FromRemoteAuthConfig(v1API.AuthConfigResponseOutput{ - PasskeyEnabled: true, - WebauthnRpDisplayName: nullable.NewNullableWithValue("Supabase CLI"), - WebauthnRpId: nullable.NewNullableWithValue("localhost"), - WebauthnRpOrigins: nullable.NewNullableWithValue("http://127.0.0.1:3000,https://localhost:3000"), - }) - // Check result - if assert.NotNil(t, c.Passkey) { - assert.True(t, c.Passkey.Enabled) - } - if assert.NotNil(t, c.Webauthn) { - assert.Equal(t, "Supabase CLI", c.Webauthn.RpDisplayName) - assert.Equal(t, "localhost", c.Webauthn.RpId) - assert.Equal(t, []string{ - "http://127.0.0.1:3000", - "https://localhost:3000", - }, c.Webauthn.RpOrigins) - } - }) - - t.Run("ignores remote settings when local passkey config is undefined", func(t *testing.T) { - c := newWithDefaults() - // Run test - c.FromRemoteAuthConfig(v1API.AuthConfigResponseOutput{ - PasskeyEnabled: true, - WebauthnRpDisplayName: nullable.NewNullableWithValue("Supabase CLI"), - WebauthnRpId: nullable.NewNullableWithValue("localhost"), - WebauthnRpOrigins: nullable.NewNullableWithValue("http://127.0.0.1:3000"), - }) - // Check result - assert.Nil(t, c.Passkey) - assert.Nil(t, c.Webauthn) - }) -} - -func TestPasskeyDiff(t *testing.T) { - t.Run("ignores undefined config", func(t *testing.T) { - c := newWithDefaults() - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - PasskeyEnabled: true, - WebauthnRpDisplayName: nullable.NewNullableWithValue("Supabase CLI"), - WebauthnRpId: nullable.NewNullableWithValue("localhost"), - WebauthnRpOrigins: nullable.NewNullableWithValue("http://127.0.0.1:3000"), - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, string(diff)) - }) -} - -func TestHookDiff(t *testing.T) { - t.Run("local and remote enabled", func(t *testing.T) { - c := newWithDefaults() - c.Hook = hook{ - BeforeUserCreated: &hookConfig{ - Enabled: true, - URI: "http://example.com", - Secrets: Secret{ - Value: "test-secret", - SHA256: "ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252", - }, - }, - CustomAccessToken: &hookConfig{ - Enabled: true, - URI: "http://example.com", - Secrets: Secret{ - Value: "test-secret", - SHA256: "ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252", - }, - }, - SendSMS: &hookConfig{ - Enabled: true, - URI: "http://example.com", - Secrets: Secret{ - Value: "test-secret", - SHA256: "ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252", - }, - }, - SendEmail: &hookConfig{ - Enabled: true, - URI: "https://example.com", - Secrets: Secret{ - Value: "test-secret", - SHA256: "ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252", - }, - }, - MFAVerificationAttempt: &hookConfig{ - Enabled: true, - URI: "https://example.com", - Secrets: Secret{ - Value: "test-secret", - SHA256: "ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252", - }, - }, - PasswordVerificationAttempt: &hookConfig{ - Enabled: true, - URI: "pg-functions://verifyPassword", - }, - } - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - HookBeforeUserCreatedEnabled: nullable.NewNullableWithValue(true), - HookBeforeUserCreatedUri: nullable.NewNullableWithValue("http://example.com"), - HookBeforeUserCreatedSecrets: nullable.NewNullableWithValue("ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"), - HookCustomAccessTokenEnabled: nullable.NewNullableWithValue(true), - HookCustomAccessTokenUri: nullable.NewNullableWithValue("http://example.com"), - HookCustomAccessTokenSecrets: nullable.NewNullableWithValue("ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"), - HookSendSmsEnabled: nullable.NewNullableWithValue(true), - HookSendSmsUri: nullable.NewNullableWithValue("http://example.com"), - HookSendSmsSecrets: nullable.NewNullableWithValue("ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"), - HookSendEmailEnabled: nullable.NewNullableWithValue(true), - HookSendEmailUri: nullable.NewNullableWithValue("https://example.com"), - HookSendEmailSecrets: nullable.NewNullableWithValue("ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"), - HookMfaVerificationAttemptEnabled: nullable.NewNullableWithValue(true), - HookMfaVerificationAttemptUri: nullable.NewNullableWithValue("https://example.com"), - HookMfaVerificationAttemptSecrets: nullable.NewNullableWithValue("ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"), - HookPasswordVerificationAttemptEnabled: nullable.NewNullableWithValue(true), - HookPasswordVerificationAttemptUri: nullable.NewNullableWithValue("pg-functions://verifyPassword"), - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, string(diff)) - }) - - t.Run("local disabled remote enabled", func(t *testing.T) { - c := newWithDefaults() - c.Hook = hook{ - BeforeUserCreated: &hookConfig{ - Enabled: false, - }, - CustomAccessToken: &hookConfig{ - Enabled: false, - }, - SendSMS: &hookConfig{ - Enabled: false, - URI: "https://example.com", - Secrets: Secret{Value: "test-secret"}, - }, - SendEmail: &hookConfig{ - Enabled: false, - }, - MFAVerificationAttempt: &hookConfig{ - Enabled: false, - URI: "pg-functions://postgres/public/verifyMFA", - }, - PasswordVerificationAttempt: nil, - } - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - HookBeforeUserCreatedEnabled: nullable.NewNullableWithValue(true), - HookBeforeUserCreatedUri: nullable.NewNullableWithValue("http://example.com"), - HookBeforeUserCreatedSecrets: nullable.NewNullableWithValue("ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"), - HookCustomAccessTokenEnabled: nullable.NewNullableWithValue(true), - HookCustomAccessTokenUri: nullable.NewNullableWithValue("http://example.com"), - HookCustomAccessTokenSecrets: nullable.NewNullableWithValue("ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"), - HookSendSmsEnabled: nullable.NewNullableWithValue(true), - HookSendSmsUri: nullable.NewNullableWithValue("https://example.com"), - HookSendSmsSecrets: nullable.NewNullableWithValue("ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"), - HookSendEmailEnabled: nullable.NewNullableWithValue(true), - HookSendEmailUri: nullable.NewNullableWithValue("pg-functions://postgres/public/sendEmail"), - HookMfaVerificationAttemptEnabled: nullable.NewNullableWithValue(true), - HookMfaVerificationAttemptUri: nullable.NewNullableWithValue("pg-functions://postgres/public/verifyMFA"), - HookPasswordVerificationAttemptEnabled: nullable.NewNullableWithValue(true), - HookPasswordVerificationAttemptUri: nullable.NewNullableWithValue("https://example.com"), - HookPasswordVerificationAttemptSecrets: nullable.NewNullableWithValue("ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"), - }) - // Check error - assert.NoError(t, err) - assertSnapshotEqual(t, diff) - }) - - t.Run("local enabled remote disabled", func(t *testing.T) { - c := newWithDefaults() - c.Hook = hook{ - BeforeUserCreated: &hookConfig{ - Enabled: true, - URI: "http://example.com", - Secrets: Secret{ - Value: "test-secret", - SHA256: "ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252", - }, - }, - CustomAccessToken: &hookConfig{ - Enabled: true, - URI: "http://example.com", - Secrets: Secret{ - Value: "test-secret", - SHA256: "ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252", - }, - }, - SendSMS: &hookConfig{ - Enabled: true, - URI: "https://example.com", - Secrets: Secret{ - Value: "test-secret", - SHA256: "ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252", - }, - }, - SendEmail: &hookConfig{ - Enabled: true, - URI: "pg-functions://postgres/public/sendEmail", - }, - MFAVerificationAttempt: &hookConfig{ - Enabled: true, - URI: "pg-functions://postgres/public/verifyMFA", - }, - PasswordVerificationAttempt: nil, - } - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - HookBeforeUserCreatedEnabled: nullable.NewNullableWithValue(false), - HookBeforeUserCreatedUri: nullable.NewNullableWithValue("pg-functions://postgres/public/beforeUserCreated"), - HookCustomAccessTokenEnabled: nullable.NewNullableWithValue(false), - HookCustomAccessTokenUri: nullable.NewNullableWithValue("pg-functions://postgres/public/customToken"), - HookSendSmsEnabled: nullable.NewNullableWithValue(false), - HookSendSmsUri: nullable.NewNullableWithValue("https://example.com"), - HookSendSmsSecrets: nullable.NewNullableWithValue("ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"), - HookSendEmailEnabled: nullable.NewNullableWithValue(false), - HookSendEmailUri: nullable.NewNullableWithValue("https://example.com"), - HookSendEmailSecrets: nullable.NewNullableWithValue("ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"), - HookMfaVerificationAttemptEnabled: nullable.NewNullableWithValue(false), - HookMfaVerificationAttemptUri: nullable.NewNullableWithValue("pg-functions://postgres/public/verifyMFA"), - HookPasswordVerificationAttemptEnabled: nullable.NewNullableWithValue(false), - }) - // Check error - assert.NoError(t, err) - assertSnapshotEqual(t, diff) - }) - - t.Run("local and remote disabled", func(t *testing.T) { - c := newWithDefaults() - c.Hook = hook{ - BeforeUserCreated: &hookConfig{Enabled: false}, - CustomAccessToken: &hookConfig{Enabled: false}, - SendSMS: &hookConfig{Enabled: false}, - SendEmail: &hookConfig{Enabled: false}, - MFAVerificationAttempt: &hookConfig{Enabled: false}, - PasswordVerificationAttempt: &hookConfig{Enabled: false}, - } - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - HookBeforeUserCreatedEnabled: nullable.NewNullableWithValue(false), - HookCustomAccessTokenEnabled: nullable.NewNullableWithValue(false), - HookSendSmsEnabled: nullable.NewNullableWithValue(false), - HookSendEmailEnabled: nullable.NewNullableWithValue(false), - HookMfaVerificationAttemptEnabled: nullable.NewNullableWithValue(false), - HookPasswordVerificationAttemptEnabled: nullable.NewNullableWithValue(false), - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, string(diff)) - }) -} - -func TestMfaDiff(t *testing.T) { - t.Run("local and remote enabled", func(t *testing.T) { - c := newWithDefaults() - c.MFA = mfa{ - TOTP: factorTypeConfiguration{ - EnrollEnabled: true, - VerifyEnabled: true, - }, - Phone: phoneFactorTypeConfiguration{ - factorTypeConfiguration: factorTypeConfiguration{ - EnrollEnabled: true, - VerifyEnabled: true, - }, - OtpLength: 6, - Template: "Your code is {{ .Code }}", - MaxFrequency: 5 * time.Second, - }, - WebAuthn: factorTypeConfiguration{ - EnrollEnabled: true, - VerifyEnabled: true, - }, - MaxEnrolledFactors: 10, - } - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - MfaMaxEnrolledFactors: nullable.NewNullableWithValue(10), - MfaTotpEnrollEnabled: nullable.NewNullableWithValue(true), - MfaTotpVerifyEnabled: nullable.NewNullableWithValue(true), - MfaPhoneEnrollEnabled: nullable.NewNullableWithValue(true), - MfaPhoneVerifyEnabled: nullable.NewNullableWithValue(true), - MfaPhoneOtpLength: 6, - MfaPhoneTemplate: nullable.NewNullableWithValue("Your code is {{ .Code }}"), - MfaPhoneMaxFrequency: nullable.NewNullableWithValue(5), - MfaWebAuthnEnrollEnabled: nullable.NewNullableWithValue(true), - MfaWebAuthnVerifyEnabled: nullable.NewNullableWithValue(true), - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, string(diff)) - }) - - t.Run("local enabled and disabled", func(t *testing.T) { - c := newWithDefaults() - c.MFA = mfa{ - TOTP: factorTypeConfiguration{ - EnrollEnabled: false, - VerifyEnabled: false, - }, - Phone: phoneFactorTypeConfiguration{ - factorTypeConfiguration: factorTypeConfiguration{ - EnrollEnabled: true, - VerifyEnabled: true, - }, - }, - } - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - MfaMaxEnrolledFactors: nullable.NewNullableWithValue(10), - MfaTotpEnrollEnabled: nullable.NewNullableWithValue(false), - MfaTotpVerifyEnabled: nullable.NewNullableWithValue(false), - MfaPhoneEnrollEnabled: nullable.NewNullableWithValue(false), - MfaPhoneVerifyEnabled: nullable.NewNullableWithValue(false), - MfaPhoneOtpLength: 6, - MfaPhoneTemplate: nullable.NewNullableWithValue("Your code is {{ .Code }}"), - MfaPhoneMaxFrequency: nullable.NewNullableWithValue(5), - MfaWebAuthnEnrollEnabled: nullable.NewNullableWithValue(false), - MfaWebAuthnVerifyEnabled: nullable.NewNullableWithValue(false), - }) - // Check error - assert.NoError(t, err) - assertSnapshotEqual(t, diff) - }) - - t.Run("local and remote disabled", func(t *testing.T) { - c := newWithDefaults() - c.MFA = mfa{ - MaxEnrolledFactors: 10, - Phone: phoneFactorTypeConfiguration{ - OtpLength: 6, - Template: "Your code is {{ .Code }}", - MaxFrequency: 5 * time.Second, - }, - } - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - MfaMaxEnrolledFactors: nullable.NewNullableWithValue(10), - MfaTotpEnrollEnabled: nullable.NewNullableWithValue(false), - MfaTotpVerifyEnabled: nullable.NewNullableWithValue(false), - MfaPhoneEnrollEnabled: nullable.NewNullableWithValue(false), - MfaPhoneVerifyEnabled: nullable.NewNullableWithValue(false), - MfaPhoneOtpLength: 6, - MfaPhoneTemplate: nullable.NewNullableWithValue("Your code is {{ .Code }}"), - MfaPhoneMaxFrequency: nullable.NewNullableWithValue(5), - MfaWebAuthnEnrollEnabled: nullable.NewNullableWithValue(false), - MfaWebAuthnVerifyEnabled: nullable.NewNullableWithValue(false), - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, string(diff)) - }) -} - -func TestEmailDiff(t *testing.T) { - t.Run("local enabled remote enabled", func(t *testing.T) { - c := newWithDefaults() - c.Email = email{ - EnableSignup: true, - DoubleConfirmChanges: true, - EnableConfirmations: true, - SecurePasswordChange: true, - Template: map[string]emailTemplate{ - "invite": { - Subject: cast.Ptr("invite-subject"), - Content: cast.Ptr("invite-content"), - }, - "confirmation": { - Subject: cast.Ptr("confirmation-subject"), - Content: cast.Ptr("confirmation-content"), - }, - "recovery": { - Subject: cast.Ptr("recovery-subject"), - Content: cast.Ptr("recovery-content"), - }, - "magic_link": { - Subject: cast.Ptr("magic-link-subject"), - Content: cast.Ptr("magic-link-content"), - }, - "email_change": { - Subject: cast.Ptr("email-change-subject"), - Content: cast.Ptr("email-change-content"), - }, - "reauthentication": { - Subject: cast.Ptr("reauthentication-subject"), - Content: cast.Ptr("reauthentication-content"), - }, - }, - Notification: map[string]notification{ - "password_changed": { - Enabled: true, - emailTemplate: emailTemplate{ - Subject: cast.Ptr("password-changed-subject"), - Content: cast.Ptr("password-changed-content"), - }, - }, - "email_changed": { - Enabled: true, - emailTemplate: emailTemplate{ - Subject: cast.Ptr("email-changed-subject"), - Content: cast.Ptr("email-changed-content"), - }, - }, - "phone_changed": { - Enabled: true, - emailTemplate: emailTemplate{ - Subject: cast.Ptr("phone-changed-subject"), - Content: cast.Ptr("phone-changed-content"), - }, - }, - "identity_linked": { - Enabled: true, - emailTemplate: emailTemplate{ - Subject: cast.Ptr("identity-linked-subject"), - Content: cast.Ptr("identity-linked-content"), - }, - }, - "identity_unlinked": { - Enabled: true, - emailTemplate: emailTemplate{ - Subject: cast.Ptr("identity-unlinked-subject"), - Content: cast.Ptr("identity-unlinked-content"), - }, - }, - "mfa_factor_enrolled": { - Enabled: true, - emailTemplate: emailTemplate{ - Subject: cast.Ptr("mfa-enrolled-subject"), - Content: cast.Ptr("mfa-enrolled-content"), - }, - }, - "mfa_factor_unenrolled": { - Enabled: true, - emailTemplate: emailTemplate{ - Subject: cast.Ptr("mfa-unenrolled-subject"), - Content: cast.Ptr("mfa-unenrolled-content"), - }, - }, - }, - Smtp: &smtp{ - Enabled: true, - Host: "smtp.sendgrid.net", - Port: 587, - User: "apikey", - Pass: Secret{ - Value: "test-key", - SHA256: "ed64b7695a606bc6ab4fcb41fe815b5ddf1063ccbc87afe1fa89756635db520e", - }, - AdminEmail: openapi_types.Email("admin@email.com"), - SenderName: "Admin", - }, - MaxFrequency: time.Second, - OtpLength: 6, - OtpExpiry: 3600, - } - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - ExternalEmailEnabled: nullable.NewNullableWithValue(true), - MailerSecureEmailChangeEnabled: nullable.NewNullableWithValue(true), - MailerAutoconfirm: nullable.NewNullableWithValue(false), - MailerOtpLength: nullable.NewNullableWithValue(6), - MailerOtpExp: 3600, - SecurityUpdatePasswordRequireReauthentication: nullable.NewNullableWithValue(true), - SmtpHost: nullable.NewNullableWithValue("smtp.sendgrid.net"), - SmtpPort: nullable.NewNullableWithValue("587"), - SmtpUser: nullable.NewNullableWithValue("apikey"), - SmtpPass: nullable.NewNullableWithValue("ed64b7695a606bc6ab4fcb41fe815b5ddf1063ccbc87afe1fa89756635db520e"), - SmtpAdminEmail: nullable.NewNullableWithValue(openapi_types.Email("admin@email.com")), - SmtpSenderName: nullable.NewNullableWithValue("Admin"), - SmtpMaxFrequency: nullable.NewNullableWithValue(1), - // Custom templates - MailerSubjectsInvite: nullable.NewNullableWithValue("invite-subject"), - MailerTemplatesInviteContent: nullable.NewNullableWithValue("invite-content"), - MailerSubjectsConfirmation: nullable.NewNullableWithValue("confirmation-subject"), - MailerTemplatesConfirmationContent: nullable.NewNullableWithValue("confirmation-content"), - MailerSubjectsRecovery: nullable.NewNullableWithValue("recovery-subject"), - MailerTemplatesRecoveryContent: nullable.NewNullableWithValue("recovery-content"), - MailerSubjectsMagicLink: nullable.NewNullableWithValue("magic-link-subject"), - MailerTemplatesMagicLinkContent: nullable.NewNullableWithValue("magic-link-content"), - MailerSubjectsEmailChange: nullable.NewNullableWithValue("email-change-subject"), - MailerTemplatesEmailChangeContent: nullable.NewNullableWithValue("email-change-content"), - MailerSubjectsReauthentication: nullable.NewNullableWithValue("reauthentication-subject"), - MailerTemplatesReauthenticationContent: nullable.NewNullableWithValue("reauthentication-content"), - // Notifications - MailerNotificationsPasswordChangedEnabled: nullable.NewNullableWithValue(true), - MailerSubjectsPasswordChangedNotification: nullable.NewNullableWithValue("password-changed-subject"), - MailerTemplatesPasswordChangedNotificationContent: nullable.NewNullableWithValue("password-changed-content"), - MailerNotificationsEmailChangedEnabled: nullable.NewNullableWithValue(true), - MailerSubjectsEmailChangedNotification: nullable.NewNullableWithValue("email-changed-subject"), - MailerTemplatesEmailChangedNotificationContent: nullable.NewNullableWithValue("email-changed-content"), - MailerNotificationsPhoneChangedEnabled: nullable.NewNullableWithValue(true), - MailerSubjectsPhoneChangedNotification: nullable.NewNullableWithValue("phone-changed-subject"), - MailerTemplatesPhoneChangedNotificationContent: nullable.NewNullableWithValue("phone-changed-content"), - MailerNotificationsIdentityLinkedEnabled: nullable.NewNullableWithValue(true), - MailerSubjectsIdentityLinkedNotification: nullable.NewNullableWithValue("identity-linked-subject"), - MailerTemplatesIdentityLinkedNotificationContent: nullable.NewNullableWithValue("identity-linked-content"), - MailerNotificationsIdentityUnlinkedEnabled: nullable.NewNullableWithValue(true), - MailerSubjectsIdentityUnlinkedNotification: nullable.NewNullableWithValue("identity-unlinked-subject"), - MailerTemplatesIdentityUnlinkedNotificationContent: nullable.NewNullableWithValue("identity-unlinked-content"), - MailerNotificationsMfaFactorEnrolledEnabled: nullable.NewNullableWithValue(true), - MailerSubjectsMfaFactorEnrolledNotification: nullable.NewNullableWithValue("mfa-enrolled-subject"), - MailerTemplatesMfaFactorEnrolledNotificationContent: nullable.NewNullableWithValue("mfa-enrolled-content"), - MailerNotificationsMfaFactorUnenrolledEnabled: nullable.NewNullableWithValue(true), - MailerSubjectsMfaFactorUnenrolledNotification: nullable.NewNullableWithValue("mfa-unenrolled-subject"), - MailerTemplatesMfaFactorUnenrolledNotificationContent: nullable.NewNullableWithValue("mfa-unenrolled-content"), - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, string(diff)) - }) - - t.Run("local enabled remote disabled", func(t *testing.T) { - c := newWithDefaults() - c.Email = email{ - EnableSignup: true, - DoubleConfirmChanges: true, - EnableConfirmations: true, - SecurePasswordChange: true, - Template: map[string]emailTemplate{ - "invite": { - Subject: cast.Ptr("invite-subject"), - Content: cast.Ptr("invite-content"), - }, - "confirmation": { - Subject: cast.Ptr("confirmation-subject"), - }, - "recovery": { - Content: cast.Ptr("recovery-content"), - }, - "magic_link": { - Subject: cast.Ptr("magic-link-subject"), - Content: cast.Ptr("magic-link-content"), - }, - "email_change": { - Subject: cast.Ptr("email-change-subject"), - Content: cast.Ptr("email-change-content"), - }, - "reauthentication": { - Subject: cast.Ptr(""), - Content: cast.Ptr(""), - }, - }, - Notification: map[string]notification{ - "password_changed": { - Enabled: true, - emailTemplate: emailTemplate{ - Subject: cast.Ptr("password-changed-subject"), - Content: cast.Ptr("password-changed-content"), - }, - }, - "email_changed": { - Enabled: true, - emailTemplate: emailTemplate{ - Subject: cast.Ptr("email-changed-subject"), - Content: cast.Ptr("email-changed-content"), - }, - }, - "phone_changed": { - Enabled: true, - emailTemplate: emailTemplate{ - Subject: cast.Ptr("phone-changed-subject"), - Content: cast.Ptr("phone-changed-content"), - }, - }, - "identity_linked": { - Enabled: true, - emailTemplate: emailTemplate{ - Subject: cast.Ptr("identity-linked-subject"), - Content: cast.Ptr("identity-linked-content"), - }, - }, - "identity_unlinked": { - Enabled: true, - }, - "mfa_factor_enrolled": { - Enabled: true, - }, - "mfa_factor_unenrolled": { - Enabled: true, - }, - }, - Smtp: &smtp{ - Enabled: true, - Host: "smtp.sendgrid.net", - Port: 587, - User: "apikey", - Pass: Secret{ - Value: "test-key", - SHA256: "ed64b7695a606bc6ab4fcb41fe815b5ddf1063ccbc87afe1fa89756635db520e", - }, - AdminEmail: openapi_types.Email("admin@email.com"), - SenderName: "Admin", - }, - MaxFrequency: time.Second, - OtpLength: 8, - OtpExpiry: 86400, - } - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - ExternalEmailEnabled: nullable.NewNullableWithValue(false), - MailerSecureEmailChangeEnabled: nullable.NewNullableWithValue(false), - MailerAutoconfirm: nullable.NewNullableWithValue(true), - MailerOtpLength: nullable.NewNullableWithValue(6), - MailerOtpExp: 3600, - SecurityUpdatePasswordRequireReauthentication: nullable.NewNullableWithValue(false), - SmtpMaxFrequency: nullable.NewNullableWithValue(60), - // Custom templates - MailerTemplatesConfirmationContent: nullable.NewNullableWithValue("confirmation-content"), - MailerSubjectsRecovery: nullable.NewNullableWithValue("recovery-subject"), - MailerSubjectsMagicLink: nullable.NewNullableWithValue("magic-link-subject"), - MailerTemplatesEmailChangeContent: nullable.NewNullableWithValue("email-change-content"), - // Notifications - MailerNotificationsPasswordChangedEnabled: nullable.NewNullableWithValue(false), - MailerSubjectsPasswordChangedNotification: nullable.NewNullableWithValue("password-changed-subject"), - MailerTemplatesPasswordChangedNotificationContent: nullable.NewNullableWithValue("password-changed-content"), - MailerNotificationsEmailChangedEnabled: nullable.NewNullableWithValue(false), - MailerSubjectsEmailChangedNotification: nullable.NewNullableWithValue("email-changed-subject"), - MailerNotificationsPhoneChangedEnabled: nullable.NewNullableWithValue(false), - MailerTemplatesPhoneChangedNotificationContent: nullable.NewNullableWithValue("phone-changed-content"), - MailerNotificationsIdentityLinkedEnabled: nullable.NewNullableWithValue(false), - MailerNotificationsIdentityUnlinkedEnabled: nullable.NewNullableWithValue(false), - MailerSubjectsIdentityUnlinkedNotification: nullable.NewNullableWithValue("identity-unlinked-subject"), - MailerTemplatesIdentityUnlinkedNotificationContent: nullable.NewNullableWithValue("identity-unlinked-content"), - MailerNotificationsMfaFactorEnrolledEnabled: nullable.NewNullableWithValue(false), - MailerSubjectsMfaFactorEnrolledNotification: nullable.NewNullableWithValue("mfa-enrolled-subject"), - MailerNotificationsMfaFactorUnenrolledEnabled: nullable.NewNullableWithValue(false), - }) - // Check error - assert.NoError(t, err) - assertSnapshotEqual(t, diff) - }) - - t.Run("local disabled remote enabled", func(t *testing.T) { - c := newWithDefaults() - c.Email = email{ - EnableConfirmations: false, - Template: map[string]emailTemplate{ - "invite": {}, - "confirmation": {}, - "recovery": {}, - "magic_link": {}, - "email_change": {}, - "reauthentication": {}, - }, - Notification: map[string]notification{ - "password_changed": {}, - "email_changed": {}, - "phone_changed": {}, - "identity_linked": {}, - "identity_unlinked": {}, - "mfa_factor_enrolled": {}, - "mfa_factor_unenrolled": {}, - }, - MaxFrequency: time.Minute, - OtpLength: 8, - OtpExpiry: 86400, - } - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - ExternalEmailEnabled: nullable.NewNullableWithValue(true), - MailerSecureEmailChangeEnabled: nullable.NewNullableWithValue(true), - MailerAutoconfirm: nullable.NewNullableWithValue(false), - MailerOtpLength: nullable.NewNullableWithValue(6), - MailerOtpExp: 3600, - SecurityUpdatePasswordRequireReauthentication: nullable.NewNullableWithValue(true), - SmtpHost: nullable.NewNullableWithValue("smtp.sendgrid.net"), - SmtpPort: nullable.NewNullableWithValue("587"), - SmtpUser: nullable.NewNullableWithValue("apikey"), - SmtpPass: nullable.NewNullableWithValue("ed64b7695a606bc6ab4fcb41fe815b5ddf1063ccbc87afe1fa89756635db520e"), - SmtpAdminEmail: nullable.NewNullableWithValue(openapi_types.Email("admin@email.com")), - SmtpSenderName: nullable.NewNullableWithValue("Admin"), - SmtpMaxFrequency: nullable.NewNullableWithValue(1), - // Custom templates - MailerSubjectsInvite: nullable.NewNullableWithValue("invite-subject"), - MailerTemplatesInviteContent: nullable.NewNullableWithValue("invite-content"), - MailerSubjectsConfirmation: nullable.NewNullableWithValue("confirmation-subject"), - MailerTemplatesConfirmationContent: nullable.NewNullableWithValue("confirmation-content"), - MailerSubjectsRecovery: nullable.NewNullableWithValue("recovery-subject"), - MailerTemplatesRecoveryContent: nullable.NewNullableWithValue("recovery-content"), - MailerSubjectsMagicLink: nullable.NewNullableWithValue("magic-link-subject"), - MailerTemplatesMagicLinkContent: nullable.NewNullableWithValue("magic-link-content"), - MailerSubjectsEmailChange: nullable.NewNullableWithValue("email-change-subject"), - MailerTemplatesEmailChangeContent: nullable.NewNullableWithValue("email-change-content"), - MailerSubjectsReauthentication: nullable.NewNullableWithValue("reauthentication-subject"), - MailerTemplatesReauthenticationContent: nullable.NewNullableWithValue("reauthentication-content"), - // Notifications - MailerNotificationsPasswordChangedEnabled: nullable.NewNullableWithValue(true), - MailerSubjectsPasswordChangedNotification: nullable.NewNullableWithValue("password-changed-subject"), - MailerTemplatesPasswordChangedNotificationContent: nullable.NewNullableWithValue("password-changed-content"), - MailerNotificationsEmailChangedEnabled: nullable.NewNullableWithValue(true), - MailerSubjectsEmailChangedNotification: nullable.NewNullableWithValue("email-changed-subject"), - MailerNotificationsPhoneChangedEnabled: nullable.NewNullableWithValue(true), - MailerTemplatesPhoneChangedNotificationContent: nullable.NewNullableWithValue("phone-changed-content"), - MailerNotificationsIdentityLinkedEnabled: nullable.NewNullableWithValue(true), - MailerNotificationsIdentityUnlinkedEnabled: nullable.NewNullableWithValue(true), - MailerSubjectsIdentityUnlinkedNotification: nullable.NewNullableWithValue("identity-unlinked-subject"), - MailerTemplatesIdentityUnlinkedNotificationContent: nullable.NewNullableWithValue("identity-unlinked-content"), - MailerNotificationsMfaFactorEnrolledEnabled: nullable.NewNullableWithValue(true), - MailerSubjectsMfaFactorEnrolledNotification: nullable.NewNullableWithValue("mfa-enrolled-subject"), - MailerTemplatesMfaFactorEnrolledNotificationContent: nullable.NewNullableWithValue("mfa-enrolled-content"), - MailerNotificationsMfaFactorUnenrolledEnabled: nullable.NewNullableWithValue(true), - MailerSubjectsMfaFactorUnenrolledNotification: nullable.NewNullableWithValue("mfa-unenrolled-subject"), - MailerTemplatesMfaFactorUnenrolledNotificationContent: nullable.NewNullableWithValue("mfa-unenrolled-content"), - }) - // Check error - assert.NoError(t, err) - assertSnapshotEqual(t, diff) - }) - - t.Run("local disabled remote disabled", func(t *testing.T) { - c := newWithDefaults() - c.Email = email{ - EnableConfirmations: false, - Template: map[string]emailTemplate{ - "invite": {}, - "confirmation": {}, - "recovery": {}, - "magic_link": {}, - "email_change": {}, - "reauthentication": {}, - }, - Notification: map[string]notification{ - "password_changed": {}, - "email_changed": {}, - "phone_changed": {}, - "identity_linked": {}, - "identity_unlinked": {}, - "mfa_factor_enrolled": {}, - "mfa_factor_unenrolled": {}, - }, - Smtp: &smtp{ - Enabled: false, - Host: "smtp.sendgrid.net", - Port: 587, - User: "apikey", - Pass: Secret{ - Value: "test-key", - SHA256: "ed64b7695a606bc6ab4fcb41fe815b5ddf1063ccbc87afe1fa89756635db520e", - }, - AdminEmail: openapi_types.Email("admin@email.com"), - SenderName: "Admin", - }, - MaxFrequency: time.Minute, - OtpLength: 6, - OtpExpiry: 3600, - } - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - ExternalEmailEnabled: nullable.NewNullableWithValue(false), - MailerSecureEmailChangeEnabled: nullable.NewNullableWithValue(false), - MailerAutoconfirm: nullable.NewNullableWithValue(true), - MailerOtpLength: nullable.NewNullableWithValue(6), - MailerOtpExp: 3600, - SecurityUpdatePasswordRequireReauthentication: nullable.NewNullableWithValue(false), - SmtpMaxFrequency: nullable.NewNullableWithValue(60), - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, string(diff)) - }) -} - -func TestSmsDiff(t *testing.T) { - t.Run("local enabled remote enabled", func(t *testing.T) { - c := newWithDefaults() - c.Sms = sms{ - EnableSignup: true, - EnableConfirmations: true, - Template: "Your code is {{ .Code }}", - TestOTP: map[string]string{"123": "456"}, - MaxFrequency: time.Minute, - Twilio: twilioConfig{ - Enabled: true, - AccountSid: "test-account", - MessageServiceSid: "test-service", - AuthToken: Secret{ - Value: "test-token", - SHA256: "c84443bc59b92caef8ec8500ff443584793756749523811eb333af2bbc74fc88", - }, - }, - } - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - ExternalPhoneEnabled: nullable.NewNullableWithValue(true), - SmsAutoconfirm: nullable.NewNullableWithValue(true), - SmsMaxFrequency: nullable.NewNullableWithValue(60), - SmsOtpExp: nullable.NewNullableWithValue(3600), - SmsOtpLength: 6, - SmsProvider: nullable.NewNullableWithValue(v1API.AuthConfigResponseOutputSmsProviderTwilio), - SmsTemplate: nullable.NewNullableWithValue("Your code is {{ .Code }}"), - SmsTestOtp: nullable.NewNullableWithValue("123=456"), - SmsTestOtpValidUntil: nullable.NewNullableWithValue(time.Date(2050, 1, 1, 1, 0, 0, 0, time.UTC)), - SmsTwilioAccountSid: nullable.NewNullableWithValue("test-account"), - SmsTwilioAuthToken: nullable.NewNullableWithValue("c84443bc59b92caef8ec8500ff443584793756749523811eb333af2bbc74fc88"), - SmsTwilioContentSid: nullable.NewNullableWithValue("test-content"), - SmsTwilioMessageServiceSid: nullable.NewNullableWithValue("test-service"), - // Extra configs returned from api can be ignored - SmsMessagebirdAccessKey: nullable.NewNullableWithValue("test-messagebird-key"), - SmsMessagebirdOriginator: nullable.NewNullableWithValue("test-messagebird-originator"), - SmsTextlocalApiKey: nullable.NewNullableWithValue("test-textlocal-key"), - SmsTextlocalSender: nullable.NewNullableWithValue("test-textlocal-sencer"), - SmsTwilioVerifyAccountSid: nullable.NewNullableWithValue("test-verify-account"), - SmsTwilioVerifyAuthToken: nullable.NewNullableWithValue("test-verify-token"), - SmsTwilioVerifyMessageServiceSid: nullable.NewNullableWithValue("test-verify-service"), - SmsVonageApiKey: nullable.NewNullableWithValue("test-vonage-key"), - SmsVonageApiSecret: nullable.NewNullableWithValue("test-vonage-secret"), - SmsVonageFrom: nullable.NewNullableWithValue("test-vonage-from"), - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, string(diff)) - }) - - t.Run("local disabled remote enabled", func(t *testing.T) { - c := newWithDefaults() - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - ExternalPhoneEnabled: nullable.NewNullableWithValue(true), - SmsAutoconfirm: nullable.NewNullableWithValue(true), - SmsMaxFrequency: nullable.NewNullableWithValue(60), - SmsOtpExp: nullable.NewNullableWithValue(3600), - SmsOtpLength: 6, - SmsProvider: nullable.NewNullableWithValue(v1API.AuthConfigResponseOutputSmsProviderTwilio), - SmsTemplate: nullable.NewNullableWithValue("Your code is {{ .Code }}"), - SmsTestOtp: nullable.NewNullableWithValue("123=456,456=123"), - SmsTestOtpValidUntil: nullable.NewNullableWithValue(time.Date(2050, 1, 1, 1, 0, 0, 0, time.UTC)), - SmsTwilioAccountSid: nullable.NewNullableWithValue("test-account"), - SmsTwilioAuthToken: nullable.NewNullableWithValue("c84443bc59b92caef8ec8500ff443584793756749523811eb333af2bbc74fc88"), - SmsTwilioContentSid: nullable.NewNullableWithValue("test-content"), - SmsTwilioMessageServiceSid: nullable.NewNullableWithValue("test-service"), - }) - // Check error - assert.NoError(t, err) - assertSnapshotEqual(t, diff) - }) - - t.Run("local enabled remote disabled", func(t *testing.T) { - c := newWithDefaults() - c.Sms = sms{ - EnableSignup: true, - EnableConfirmations: true, - Template: "Your code is {{ .Code }}", - TestOTP: map[string]string{"123": "456"}, - MaxFrequency: time.Minute, - Messagebird: messagebirdConfig{ - Enabled: true, - Originator: "test-originator", - AccessKey: Secret{ - Value: "test-access-key", - SHA256: "ab60d03fc809fb02dae838582f3ddc13d1d6cb32ffba77c4b969dd3caa496f13", - }, - }, - } - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - ExternalPhoneEnabled: nullable.NewNullableWithValue(false), - SmsAutoconfirm: nullable.NewNullableWithValue(false), - SmsMaxFrequency: nullable.NewNullableWithValue(0), - SmsOtpExp: nullable.NewNullableWithValue(3600), - SmsOtpLength: 6, - SmsProvider: nullable.NewNullableWithValue(v1API.AuthConfigResponseOutputSmsProviderTwilio), - SmsTemplate: nullable.NewNullableWithValue(""), - SmsTwilioAccountSid: nullable.NewNullableWithValue("test-account"), - SmsTwilioAuthToken: nullable.NewNullableWithValue("c84443bc59b92caef8ec8500ff443584793756749523811eb333af2bbc74fc88"), - SmsTwilioContentSid: nullable.NewNullableWithValue("test-content"), - SmsTwilioMessageServiceSid: nullable.NewNullableWithValue("test-service"), - }) - // Check error - assert.NoError(t, err) - assertSnapshotEqual(t, diff) - }) - - t.Run("local disabled remote disabled", func(t *testing.T) { - c := newWithDefaults() - c.Sms = sms{ - EnableSignup: false, - EnableConfirmations: true, - Template: "Your code is {{ .Code }}", - TestOTP: map[string]string{"123": "456"}, - MaxFrequency: time.Minute, - } - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - ExternalPhoneEnabled: nullable.NewNullableWithValue(false), - SmsAutoconfirm: nullable.NewNullableWithValue(true), - SmsMaxFrequency: nullable.NewNullableWithValue(60), - SmsOtpExp: nullable.NewNullableWithValue(3600), - SmsOtpLength: 6, - SmsTemplate: nullable.NewNullableWithValue("Your code is {{ .Code }}"), - SmsTestOtp: nullable.NewNullableWithValue("123=456"), - SmsTestOtpValidUntil: nullable.NewNullableWithValue(time.Date(2050, 1, 1, 1, 0, 0, 0, time.UTC)), - SmsProvider: nullable.NewNullableWithValue(v1API.AuthConfigResponseOutputSmsProviderMessagebird), - SmsMessagebirdAccessKey: nullable.NewNullableWithValue("test-messagebird-key"), - SmsMessagebirdOriginator: nullable.NewNullableWithValue("test-messagebird-originator"), - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, string(diff)) - }) - - t.Run("enable sign up without provider", func(t *testing.T) { - // This is not a valid config because platform requires a SMS provider. - // For consistency, we handle this in config.Load and emit a warning. - c := newWithDefaults() - c.Sms.EnableSignup = true - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - ExternalPhoneEnabled: nullable.NewNullableWithValue(false), - SmsProvider: nullable.NewNullableWithValue(v1API.AuthConfigResponseOutputSmsProviderTwilio), - }) - // Check error - assert.NoError(t, err) - assertSnapshotEqual(t, diff) - }) - - t.Run("enable provider without sign up", func(t *testing.T) { - c := newWithDefaults() - c.Sms.Messagebird.Enabled = true - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - ExternalPhoneEnabled: nullable.NewNullableWithValue(false), - SmsProvider: nullable.NewNullableWithValue(v1API.AuthConfigResponseOutputSmsProviderMessagebird), - SmsMessagebirdAccessKey: nullable.NewNullableWithValue(""), - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, string(diff)) - }) -} - -func TestExternalDiff(t *testing.T) { - t.Run("local and remote enabled", func(t *testing.T) { - c := newWithDefaults() - c.External = map[string]provider{ - "apple": {Enabled: true}, - "azure": {Enabled: true}, - "bitbucket": {Enabled: true}, - "discord": {Enabled: true}, - "facebook": {Enabled: true}, - "figma": {Enabled: true}, - "github": {Enabled: true}, - "gitlab": {Enabled: true}, - "google": {Enabled: true}, - "kakao": {Enabled: true}, - "keycloak": {Enabled: true}, - "linkedin_oidc": {Enabled: true}, - "notion": {Enabled: true}, - "slack_oidc": {Enabled: true}, - "spotify": {Enabled: true}, - "twitch": {Enabled: true}, - "twitter": {Enabled: true}, - "x": {Enabled: true}, - "workos": {Enabled: true}, - "zoom": {Enabled: true}, - } - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - ExternalAppleAdditionalClientIds: nullable.NewNullableWithValue(""), - ExternalAppleClientId: nullable.NewNullableWithValue(""), - ExternalAppleEnabled: nullable.NewNullableWithValue(true), - ExternalAppleSecret: nullable.NewNullableWithValue(""), - ExternalAzureClientId: nullable.NewNullableWithValue(""), - ExternalAzureEnabled: nullable.NewNullableWithValue(true), - ExternalAzureSecret: nullable.NewNullableWithValue(""), - ExternalAzureUrl: nullable.NewNullableWithValue(""), - ExternalBitbucketClientId: nullable.NewNullableWithValue(""), - ExternalBitbucketEnabled: nullable.NewNullableWithValue(true), - ExternalBitbucketSecret: nullable.NewNullableWithValue(""), - ExternalDiscordClientId: nullable.NewNullableWithValue(""), - ExternalDiscordEnabled: nullable.NewNullableWithValue(true), - ExternalDiscordSecret: nullable.NewNullableWithValue(""), - ExternalFacebookClientId: nullable.NewNullableWithValue(""), - ExternalFacebookEnabled: nullable.NewNullableWithValue(true), - ExternalFacebookSecret: nullable.NewNullableWithValue(""), - ExternalFigmaClientId: nullable.NewNullableWithValue(""), - ExternalFigmaEnabled: nullable.NewNullableWithValue(true), - ExternalFigmaSecret: nullable.NewNullableWithValue(""), - ExternalGithubClientId: nullable.NewNullableWithValue(""), - ExternalGithubEnabled: nullable.NewNullableWithValue(true), - ExternalGithubSecret: nullable.NewNullableWithValue(""), - ExternalGitlabClientId: nullable.NewNullableWithValue(""), - ExternalGitlabEnabled: nullable.NewNullableWithValue(true), - ExternalGitlabSecret: nullable.NewNullableWithValue(""), - ExternalGitlabUrl: nullable.NewNullableWithValue(""), - ExternalGoogleAdditionalClientIds: nullable.NewNullableWithValue(""), - ExternalGoogleClientId: nullable.NewNullableWithValue(""), - ExternalGoogleEnabled: nullable.NewNullableWithValue(true), - ExternalGoogleSecret: nullable.NewNullableWithValue(""), - ExternalGoogleSkipNonceCheck: nullable.NewNullableWithValue(false), - ExternalKakaoClientId: nullable.NewNullableWithValue(""), - ExternalKakaoEnabled: nullable.NewNullableWithValue(true), - ExternalKakaoSecret: nullable.NewNullableWithValue(""), - ExternalKeycloakClientId: nullable.NewNullableWithValue(""), - ExternalKeycloakEnabled: nullable.NewNullableWithValue(true), - ExternalKeycloakSecret: nullable.NewNullableWithValue(""), - ExternalKeycloakUrl: nullable.NewNullableWithValue(""), - ExternalLinkedinOidcClientId: nullable.NewNullableWithValue(""), - ExternalLinkedinOidcEnabled: nullable.NewNullableWithValue(true), - ExternalLinkedinOidcSecret: nullable.NewNullableWithValue(""), - ExternalNotionClientId: nullable.NewNullableWithValue(""), - ExternalNotionEnabled: nullable.NewNullableWithValue(true), - ExternalNotionSecret: nullable.NewNullableWithValue(""), - ExternalSlackOidcClientId: nullable.NewNullableWithValue(""), - ExternalSlackOidcEnabled: nullable.NewNullableWithValue(true), - ExternalSlackOidcSecret: nullable.NewNullableWithValue(""), - ExternalSpotifyClientId: nullable.NewNullableWithValue(""), - ExternalSpotifyEnabled: nullable.NewNullableWithValue(true), - ExternalSpotifySecret: nullable.NewNullableWithValue(""), - ExternalTwitchClientId: nullable.NewNullableWithValue(""), - ExternalTwitchEnabled: nullable.NewNullableWithValue(true), - ExternalTwitchSecret: nullable.NewNullableWithValue(""), - ExternalTwitterClientId: nullable.NewNullableWithValue(""), - ExternalTwitterEnabled: nullable.NewNullableWithValue(true), - ExternalTwitterSecret: nullable.NewNullableWithValue(""), - ExternalXClientId: nullable.NewNullableWithValue(""), - ExternalXEnabled: nullable.NewNullableWithValue(true), - ExternalXSecret: nullable.NewNullableWithValue(""), - ExternalWorkosClientId: nullable.NewNullableWithValue(""), - ExternalWorkosEnabled: nullable.NewNullableWithValue(true), - ExternalWorkosSecret: nullable.NewNullableWithValue(""), - ExternalWorkosUrl: nullable.NewNullableWithValue(""), - ExternalZoomClientId: nullable.NewNullableWithValue(""), - ExternalZoomEnabled: nullable.NewNullableWithValue(true), - ExternalZoomSecret: nullable.NewNullableWithValue(""), - // Deprecated fields should be ignored - ExternalSlackClientId: nullable.NewNullableWithValue(""), - ExternalSlackEnabled: nullable.NewNullableWithValue(true), - ExternalSlackSecret: nullable.NewNullableWithValue(""), - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, string(diff)) - }) - - t.Run("local enabled and disabled", func(t *testing.T) { - c := newWithDefaults() - c.External = map[string]provider{ - "apple": { - Enabled: true, - ClientId: "test-client-1,test-client-2", - Secret: Secret{ - Value: "test-secret", - SHA256: "ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252", - }, - }, - "azure": { - Enabled: true, - ClientId: "test-client-1", - Secret: Secret{ - Value: "test-secret", - SHA256: "ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252", - }, - }, - "bitbucket": {}, - "discord": {}, - "facebook": {}, - "figma": {}, - "github": {}, - "gitlab": {}, - "google": { - Enabled: false, - ClientId: "test-client-2", - Secret: Secret{Value: "env(test_secret)"}, - SkipNonceCheck: false, - }, - // "kakao": {}, - "keycloak": {}, - "linkedin_oidc": {}, - "notion": {}, - "slack_oidc": {}, - "spotify": {}, - "twitch": {}, - "twitter": {}, - "x": {}, - "workos": {}, - "zoom": {}, - } - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - ExternalAppleAdditionalClientIds: nullable.NewNullableWithValue("test-client-2"), - ExternalAppleClientId: nullable.NewNullableWithValue("test-client-1"), - ExternalAppleEnabled: nullable.NewNullableWithValue(false), - ExternalAppleSecret: nullable.NewNullableWithValue("ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"), - ExternalGoogleAdditionalClientIds: nullable.NewNullableWithValue("test-client-2"), - ExternalGoogleClientId: nullable.NewNullableWithValue("test-client-1"), - ExternalGoogleEnabled: nullable.NewNullableWithValue(true), - ExternalGoogleSecret: nullable.NewNullableWithValue("b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad"), - ExternalGoogleSkipNonceCheck: nullable.NewNullableWithValue(true), - ExternalKakaoClientId: nullable.NewNullableWithValue("test-client-2"), - ExternalKakaoEnabled: nullable.NewNullableWithValue(true), - ExternalKakaoSecret: nullable.NewNullableWithValue("b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad"), - }) - // Check error - assert.NoError(t, err) - assertSnapshotEqual(t, diff) - }) - - t.Run("local and remote disabled", func(t *testing.T) { - c := newWithDefaults() - c.External = map[string]provider{ - "apple": {}, - "azure": {}, - "bitbucket": {}, - "discord": {}, - "facebook": {}, - "figma": {}, - "github": {}, - "gitlab": {}, - "google": {}, - "kakao": {}, - "keycloak": {}, - "linkedin_oidc": {}, - "notion": {}, - "slack_oidc": {}, - "spotify": {}, - "twitch": {}, - "twitter": {}, - "x": {}, - "workos": {}, - "zoom": {}, - } - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - ExternalAppleEnabled: nullable.NewNullableWithValue(false), - ExternalAzureEnabled: nullable.NewNullableWithValue(false), - ExternalBitbucketEnabled: nullable.NewNullableWithValue(false), - ExternalDiscordEnabled: nullable.NewNullableWithValue(false), - ExternalFacebookEnabled: nullable.NewNullableWithValue(false), - ExternalFigmaEnabled: nullable.NewNullableWithValue(false), - ExternalGithubEnabled: nullable.NewNullableWithValue(false), - ExternalGitlabEnabled: nullable.NewNullableWithValue(false), - ExternalGoogleEnabled: nullable.NewNullableWithValue(false), - ExternalGoogleSkipNonceCheck: nullable.NewNullableWithValue(false), - ExternalKakaoEnabled: nullable.NewNullableWithValue(false), - ExternalKeycloakEnabled: nullable.NewNullableWithValue(false), - ExternalLinkedinOidcEnabled: nullable.NewNullableWithValue(false), - ExternalNotionEnabled: nullable.NewNullableWithValue(false), - ExternalSlackOidcEnabled: nullable.NewNullableWithValue(false), - ExternalSpotifyEnabled: nullable.NewNullableWithValue(false), - ExternalTwitchEnabled: nullable.NewNullableWithValue(false), - ExternalTwitterEnabled: nullable.NewNullableWithValue(false), - ExternalXEnabled: nullable.NewNullableWithValue(false), - ExternalWorkosEnabled: nullable.NewNullableWithValue(false), - ExternalZoomEnabled: nullable.NewNullableWithValue(false), - // Deprecated fields should be ignored - ExternalSlackEnabled: nullable.NewNullableWithValue(false), - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, string(diff)) - }) -} - -func TestRateLimitsDiff(t *testing.T) { - t.Run("local and remote rate limits match", func(t *testing.T) { - // Setup auth with rate limits - c := newWithDefaults() - c.RateLimit.AnonymousUsers = 20 - c.RateLimit.TokenRefresh = 30 - c.RateLimit.SignInSignUps = 40 - c.RateLimit.TokenVerifications = 50 - c.RateLimit.EmailSent = 25 - c.RateLimit.SmsSent = 35 - c.Email.Smtp = &smtp{Enabled: true} - // Run test - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - RateLimitAnonymousUsers: nullable.NewNullableWithValue(20), - RateLimitTokenRefresh: nullable.NewNullableWithValue(30), - RateLimitOtp: nullable.NewNullableWithValue(40), - RateLimitVerify: nullable.NewNullableWithValue(50), - RateLimitEmailSent: nullable.NewNullableWithValue(25), - RateLimitSmsSent: nullable.NewNullableWithValue(35), - SmtpHost: nullable.NewNullableWithValue(""), - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, string(diff)) - }) - - t.Run("local and remote rate limits differ", func(t *testing.T) { - // Setup auth with rate limits - c := newWithDefaults() - c.RateLimit.AnonymousUsers = 20 - c.RateLimit.TokenRefresh = 30 - c.RateLimit.SignInSignUps = 40 - c.RateLimit.TokenVerifications = 50 - c.RateLimit.EmailSent = 25 - c.RateLimit.SmsSent = 35 - c.Email.Smtp = &smtp{Enabled: true} - // Run test with different remote values - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - RateLimitAnonymousUsers: nullable.NewNullableWithValue(10), // Different value - RateLimitTokenRefresh: nullable.NewNullableWithValue(30), - RateLimitOtp: nullable.NewNullableWithValue(45), // Different value - RateLimitVerify: nullable.NewNullableWithValue(50), - RateLimitEmailSent: nullable.NewNullableWithValue(15), // Different value - RateLimitSmsSent: nullable.NewNullableWithValue(55), // Different value - SmtpHost: nullable.NewNullableWithValue(""), - }) - // Check error - assert.NoError(t, err) - // Compare with snapshot - assertSnapshotEqual(t, diff) - }) - - t.Run("ignores email rate limit when smtp is disabled", func(t *testing.T) { - // Setup auth without rate limits - c := newWithDefaults() - c.RateLimit.EmailSent = 25 - // Run test with remote rate limits - diff, err := c.DiffWithRemote(v1API.AuthConfigResponseOutput{ - RateLimitEmailSent: nullable.NewNullableWithValue(15), - SmtpHost: nullable.NewNullableWithValue(""), - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, string(diff)) - }) -} diff --git a/apps/cli-go/pkg/config/config.go b/apps/cli-go/pkg/config/config.go deleted file mode 100644 index d9a36221a4..0000000000 --- a/apps/cli-go/pkg/config/config.go +++ /dev/null @@ -1,1911 +0,0 @@ -package config - -import ( - "bytes" - "context" - _ "embed" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "io/fs" - "maps" - "net" - "net/http" - "net/url" - "os" - "path" - "path/filepath" - "regexp" - "slices" - "sort" - "strconv" - "strings" - "text/template" - "time" - - "github.com/BurntSushi/toml" - "github.com/docker/go-units" - "github.com/go-errors/errors" - "github.com/go-viper/mapstructure/v2" - "github.com/joho/godotenv" - "github.com/spf13/viper" - "github.com/supabase/cli/pkg/cast" - "github.com/supabase/cli/pkg/fetcher" - "golang.org/x/mod/semver" -) - -// Type for turning human-friendly bytes string ("5MB", "32kB") into an int64 during toml decoding. -type sizeInBytes int64 - -func (s *sizeInBytes) UnmarshalText(text []byte) error { - size, err := units.RAMInBytes(string(text)) - if err == nil { - *s = sizeInBytes(size) - } - return err -} - -func (s sizeInBytes) MarshalText() (text []byte, err error) { - return []byte(units.BytesSize(float64(s))), nil -} - -type LogflareBackend string - -const ( - LogflarePostgres LogflareBackend = "postgres" - LogflareBigQuery LogflareBackend = "bigquery" -) - -func (b *LogflareBackend) UnmarshalText(text []byte) error { - allowed := []LogflareBackend{LogflarePostgres, LogflareBigQuery} - if *b = LogflareBackend(text); !slices.Contains(allowed, *b) { - return errors.Errorf("must be one of %v", allowed) - } - return nil -} - -type AddressFamily string - -const ( - AddressIPv6 AddressFamily = "IPv6" - AddressIPv4 AddressFamily = "IPv4" -) - -func (f *AddressFamily) UnmarshalText(text []byte) error { - allowed := []AddressFamily{AddressIPv6, AddressIPv4} - if *f = AddressFamily(text); !slices.Contains(allowed, *f) { - return errors.Errorf("must be one of %v", allowed) - } - return nil -} - -type RequestPolicy string - -const ( - PolicyPerWorker RequestPolicy = "per_worker" - PolicyOneshot RequestPolicy = "oneshot" -) - -func (p *RequestPolicy) UnmarshalText(text []byte) error { - allowed := []RequestPolicy{PolicyPerWorker, PolicyOneshot} - if *p = RequestPolicy(text); !slices.Contains(allowed, *p) { - return errors.Errorf("must be one of %v", allowed) - } - return nil -} - -type Glob []string - -type globOptions struct { - skipEmptyGlobs bool - errorOnAllSkipped bool -} - -type GlobOption func(*globOptions) - -func WithSkipEmptyGlobs() GlobOption { - return func(o *globOptions) { - o.skipEmptyGlobs = true - } -} - -func WithErrorOnAllSkippedGlobs() GlobOption { - return func(o *globOptions) { - o.errorOnAllSkipped = true - } -} - -func (g Glob) Files(fsys fs.FS, options ...GlobOption) ([]string, error) { - return g.files(fsys, nil, options...) -} - -// SQLFiles matches glob patterns and expands directory matches recursively to -// SQL files. Pattern order is preserved, and directory contents are sorted for -// deterministic application. -func (g Glob) SQLFiles(fsys fs.FS, options ...GlobOption) ([]string, error) { - return g.files(fsys, func(path string, entry fs.DirEntry) bool { - return entry.Type().IsRegular() && filepath.Ext(path) == ".sql" - }, options...) -} - -// Match the glob patterns in the given FS to get a deduplicated -// array of all migrations files to apply in the declared order. -func (g Glob) files(fsys fs.FS, expandDir func(string, fs.DirEntry) bool, options ...GlobOption) ([]string, error) { - opts := globOptions{} - for _, apply := range options { - apply(&opts) - } - var result []string - var allErrors []error - var skipped []string - set := make(map[string]struct{}) - for _, pattern := range g { - // Glob expects / as path separator on windows - matches, err := fs.Glob(fsys, filepath.ToSlash(pattern)) - if err != nil { - allErrors = append(allErrors, errors.Errorf("failed to glob files: %w", err)) - } else if len(matches) == 0 { - if opts.skipEmptyGlobs && hasGlobMeta(pattern) { - skipped = append(skipped, pattern) - continue - } - allErrors = append(allErrors, errors.Errorf("no files matched pattern: %s", pattern)) - } - sort.Strings(matches) - // Remove duplicates - for _, item := range matches { - fp := filepath.ToSlash(item) - if expandDir != nil { - info, err := fs.Stat(fsys, fp) - if err != nil { - allErrors = append(allErrors, errors.Errorf("failed to stat matched file: %w", err)) - continue - } - if info.IsDir() { - files, err := walkMatchedDir(fsys, fp, expandDir) - if err != nil { - allErrors = append(allErrors, err) - continue - } - for _, file := range files { - if _, exists := set[file]; !exists { - set[file] = struct{}{} - result = append(result, file) - } - } - continue - } - } - if _, exists := set[fp]; !exists { - set[fp] = struct{}{} - result = append(result, fp) - } - } - } - if opts.errorOnAllSkipped && len(result) == 0 && len(skipped) > 0 { - for _, pattern := range skipped { - allErrors = append(allErrors, errors.Errorf("no files matched pattern: %s", pattern)) - } - } - return result, errors.Join(allErrors...) -} - -func walkMatchedDir(fsys fs.FS, dir string, include func(string, fs.DirEntry) bool) ([]string, error) { - var files []string - if err := fs.WalkDir(fsys, dir, func(path string, entry fs.DirEntry, err error) error { - if err != nil { - return err - } - if include(path, entry) { - files = append(files, filepath.ToSlash(path)) - } - return nil - }); err != nil { - return nil, errors.Errorf("failed to walk matched directory: %w", err) - } - sort.Strings(files) - return files, nil -} - -func hasGlobMeta(pattern string) bool { - return strings.ContainsAny(pattern, `*?[`) -} - -// We follow these rules when adding new config: -// 1. Update init_config.toml (and init_config.test.toml) with the new key, default value, and comments to explain usage. -// 2. Update config struct with new field and toml tag (spelled in snake_case). -// 3. Add custom field validations to LoadConfigFS function for eg. integer range checks. -// -// If you are adding new user defined secrets, such as OAuth provider secret, the default value in -// init_config.toml should be an env var substitution. For example, -// -// > secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" -// -// Default values for internal configs should be added to `var Config` initializer. -type ( - // Common config fields between our "base" config and any "remote" branch specific - baseConfig struct { - ProjectId string `toml:"project_id" json:"project_id"` - Hostname string `toml:"-" json:"-"` - Api api `toml:"api" json:"api"` - Db db `toml:"db" json:"db"` - Realtime realtime `toml:"realtime" json:"realtime"` - Studio studio `toml:"studio" json:"studio"` - Inbucket inbucket `toml:"local_smtp" json:"local_smtp"` - Storage storage `toml:"storage" json:"storage"` - Auth auth `toml:"auth" json:"auth"` - EdgeRuntime edgeRuntime `toml:"edge_runtime" json:"edge_runtime"` - Functions FunctionConfig `toml:"functions" json:"functions"` - Analytics analytics `toml:"analytics" json:"analytics"` - Experimental experimental `toml:"experimental" json:"experimental"` - // Compute is parsed but never read here. The [compute] section is owned by - // the TS CLI; this field exists only so a config the TS schema accepts does - // not trip UnmarshalExact in the delegated Go child (`flags.LoadConfig`). - // The json tag is the one that matters: the decoder runs with - // dc.TagName = "json". Omitted from toml so Go never emits the section. - Compute map[string]any `toml:"-" json:"compute"` - // Local is parsed but never read here; [local] is owned by the TS CLI (see Compute). - Local map[string]any `toml:"-" json:"local"` - } - - config struct { - baseConfig - Remotes map[string]baseConfig `toml:"remotes" json:"remotes"` - } - - realtime struct { - Enabled bool `toml:"enabled" json:"enabled"` - Image string `toml:"-" json:"-"` - IpVersion AddressFamily `toml:"ip_version" json:"ip_version"` - MaxHeaderLength uint `toml:"max_header_length" json:"max_header_length"` - TenantId string `toml:"-" json:"-"` - EncryptionKey string `toml:"-" json:"-"` - SecretKeyBase string `toml:"-" json:"-"` - } - - studio struct { - Enabled bool `toml:"enabled" json:"enabled"` - Image string `toml:"-" json:"-"` - Port uint16 `toml:"port" json:"port"` - ApiUrl string `toml:"api_url" json:"api_url"` - OpenaiApiKey Secret `toml:"openai_api_key" json:"openai_api_key"` - PgmetaImage string `toml:"-" json:"-"` - } - - inbucket struct { - Enabled bool `toml:"enabled" json:"enabled"` - Image string `toml:"-" json:"-"` - Port uint16 `toml:"port" json:"port"` - SmtpPort uint16 `toml:"smtp_port" json:"smtp_port"` - Pop3Port uint16 `toml:"pop3_port" json:"pop3_port"` - AdminEmail string `toml:"admin_email" json:"admin_email"` - SenderName string `toml:"sender_name" json:"sender_name"` - } - - edgeRuntime struct { - Enabled bool `toml:"enabled" json:"enabled"` - Image string `toml:"-" json:"-"` - Policy RequestPolicy `toml:"policy" json:"policy"` - InspectorPort uint16 `toml:"inspector_port" json:"inspector_port"` - Secrets SecretsConfig `toml:"secrets" json:"secrets"` - DenoVersion uint `toml:"deno_version" json:"deno_version"` - } - - SecretsConfig map[string]Secret - FunctionConfig map[string]function - - function struct { - Enabled bool `toml:"enabled" json:"enabled"` - VerifyJWT *bool `toml:"verify_jwt" json:"verify_jwt"` - ImportMap string `toml:"import_map" json:"import_map"` - Entrypoint string `toml:"entrypoint" json:"entrypoint"` - StaticFiles Glob `toml:"static_files" json:"static_files"` - } - - analytics struct { - Enabled bool `toml:"enabled" json:"enabled"` - Image string `toml:"-" json:"-"` - VectorImage string `toml:"-" json:"-"` - Port uint16 `toml:"port" json:"port"` - Backend LogflareBackend `toml:"backend" json:"backend"` - GcpProjectId string `toml:"gcp_project_id" json:"gcp_project_id"` - GcpProjectNumber string `toml:"gcp_project_number" json:"gcp_project_number"` - GcpJwtPath string `toml:"gcp_jwt_path" json:"gcp_jwt_path"` - ApiKey string `toml:"-" json:"-"` - // Deprecated together with syslog - VectorPort uint16 `toml:"vector_port" json:"vector_port"` - } - - webhooks struct { - Enabled bool `toml:"enabled" json:"enabled"` - } - - PgDeltaConfig struct { - Enabled bool `toml:"enabled" json:"enabled"` - DeclarativeSchemaPath string `toml:"declarative_schema_path" json:"declarative_schema_path"` - FormatOptions string `toml:"format_options" json:"format_options"` - } - - inspect struct { - Rules []rule `toml:"rules" json:"rules"` - } - - rule struct { - Query string `toml:"query" json:"query"` - Name string `toml:"name" json:"name"` - Pass string `toml:"pass" json:"pass"` - Fail string `toml:"fail" json:"fail"` - } - - experimental struct { - OrioleDBVersion string `toml:"orioledb_version" json:"orioledb_version"` - S3Host string `toml:"s3_host" json:"s3_host"` - S3Region string `toml:"s3_region" json:"s3_region"` - S3AccessKey string `toml:"s3_access_key" json:"s3_access_key"` - S3SecretKey string `toml:"s3_secret_key" json:"s3_secret_key"` - Stack bool `toml:"-" json:"stack"` - Compute bool `toml:"-" json:"compute"` - Webhooks *webhooks `toml:"webhooks" json:"webhooks"` - PgDelta *PgDeltaConfig `toml:"pgdelta" json:"pgdelta"` - Inspect inspect `toml:"inspect" json:"inspect"` - // PgDeltaInitEnabled drives the [experimental.pgdelta] enabled value rendered - // by Eject. It is true only for the supabase init scaffold so freshly generated - // projects opt into pg-delta, and false when Eject feeds mergeDefaultValues so - // existing configs without the section keep resolving to migra (non-breaking). - PgDeltaInitEnabled bool `toml:"-" json:"-"` - } -) - -func (a *auth) Clone() auth { - copy := *a - if copy.Captcha != nil { - capt := *a.Captcha - copy.Captcha = &capt - } - if copy.Passkey != nil { - passkey := *a.Passkey - copy.Passkey = &passkey - } - if copy.Webauthn != nil { - webauthn := *a.Webauthn - webauthn.RpOrigins = slices.Clone(a.Webauthn.RpOrigins) - copy.Webauthn = &webauthn - } - copy.External = maps.Clone(a.External) - if a.Email.Smtp != nil { - mailer := *a.Email.Smtp - copy.Email.Smtp = &mailer - } - copy.Email.Template = maps.Clone(a.Email.Template) - copy.Email.Notification = maps.Clone(a.Email.Notification) - if a.Hook.MFAVerificationAttempt != nil { - hook := *a.Hook.MFAVerificationAttempt - copy.Hook.MFAVerificationAttempt = &hook - } - if a.Hook.PasswordVerificationAttempt != nil { - hook := *a.Hook.PasswordVerificationAttempt - copy.Hook.PasswordVerificationAttempt = &hook - } - if a.Hook.CustomAccessToken != nil { - hook := *a.Hook.CustomAccessToken - copy.Hook.CustomAccessToken = &hook - } - if a.Hook.SendSMS != nil { - hook := *a.Hook.SendSMS - copy.Hook.SendSMS = &hook - } - if a.Hook.SendEmail != nil { - hook := *a.Hook.SendEmail - copy.Hook.SendEmail = &hook - } - if a.Hook.BeforeUserCreated != nil { - hook := *a.Hook.BeforeUserCreated - copy.Hook.BeforeUserCreated = &hook - } - copy.Sms.TestOTP = maps.Clone(a.Sms.TestOTP) - return copy -} - -func (s *storage) Clone() storage { - copy := *s - copy.Buckets = maps.Clone(s.Buckets) - copy.AnalyticsBuckets.Buckets = maps.Clone(s.AnalyticsBuckets.Buckets) - copy.VectorBuckets.Buckets = maps.Clone(s.VectorBuckets.Buckets) - if s.ImageTransformation != nil { - img := *s.ImageTransformation - copy.ImageTransformation = &img - } - if s.S3Protocol != nil { - s3 := *s.S3Protocol - copy.S3Protocol = &s3 - } - return copy -} - -func (c *baseConfig) Clone() baseConfig { - copy := *c - copy.Db.Vault = maps.Clone(c.Db.Vault) - copy.Storage = c.Storage.Clone() - copy.EdgeRuntime.Secrets = maps.Clone(c.EdgeRuntime.Secrets) - copy.Functions = maps.Clone(c.Functions) - copy.Auth = c.Auth.Clone() - if c.Experimental.Webhooks != nil { - webhooks := *c.Experimental.Webhooks - copy.Experimental.Webhooks = &webhooks - } - if c.Experimental.PgDelta != nil { - pgDelta := *c.Experimental.PgDelta - copy.Experimental.PgDelta = &pgDelta - } - return copy -} - -type Config *config - -type ConfigEditor func(Config) - -func WithHostname(hostname string) ConfigEditor { - return func(c Config) { - c.Hostname = hostname - } -} - -func NewConfig(editors ...ConfigEditor) config { - initial := config{baseConfig: baseConfig{ - Hostname: "127.0.0.1", - Api: api{ - Image: Images.Postgrest, - KongImage: Images.Kong, - Tls: tlsKong{ - CertContent: kongCert, - KeyContent: kongKey, - }, - }, - Db: db{ - Image: Images.Pg, - Password: "postgres", - RootKey: Secret{ - Value: "d4dc5b6d4a1d6a10b2c1e76112c994d65db7cec380572cc1839624d4be3fa275", - }, - Pooler: pooler{ - Image: Images.Supavisor, - TenantId: "pooler-dev", - EncryptionKey: "12345678901234567890123456789032", - SecretKeyBase: "EAx3IQ/wRG1v47ZD4NE4/9RzBI8Jmil3x0yhcW4V2NHBP6c2iPIzwjofi2Ep4HIG", - }, - Migrations: migrations{ - Enabled: true, - }, - Seed: seed{ - Enabled: true, - SqlPaths: []string{"seed.sql"}, - }, - }, - Realtime: realtime{ - Image: Images.Realtime, - IpVersion: AddressIPv4, - MaxHeaderLength: 4096, - TenantId: "realtime-dev", - EncryptionKey: "supabaserealtime", - SecretKeyBase: "EAx3IQ/wRG1v47ZD4NE4/9RzBI8Jmil3x0yhcW4V2NHBP6c2iPIzwjofi2Ep4HIG", - }, - Storage: storage{ - Image: Images.Storage, - ImgProxyImage: Images.ImgProxy, - S3Credentials: storageS3Credentials{ - AccessKeyId: "625729a08b95bf1b7ff351a663f3a23c", - SecretAccessKey: "850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907", - Region: "local", - }, - }, - Auth: auth{ - Image: Images.Gotrue, - Email: email{ - Template: map[string]emailTemplate{}, - Notification: map[string]notification{}, - }, - Sms: sms{ - TestOTP: map[string]string{}, - }, - External: map[string]provider{}, - SigningKeys: []JWK{{ - KeyType: "EC", - KeyID: "b81269f1-21d8-4f2e-b719-c2240a840d90", - Use: "sig", - KeyOps: []string{"sign", "verify"}, - Algorithm: "ES256", - Extractable: cast.Ptr(true), - Curve: "P-256", - X: "M5Sjqn5zwC9Kl1zVfUUGvv9boQjCGd45G8sdopBExB4", - Y: "P6IXMvA2WYXSHSOMTBH2jsw_9rrzGy89FjPf6oOsIxQ", - PrivateExponent: "dIhR8wywJlqlua4y_yMq2SLhlFXDZJBCvFrY1DCHyVU", - }}, - }, - Inbucket: inbucket{ - Image: Images.Inbucket, - AdminEmail: "admin@email.com", - SenderName: "Admin", - }, - Studio: studio{ - Image: Images.Studio, - PgmetaImage: Images.Pgmeta, - }, - Analytics: analytics{ - Image: Images.Logflare, - VectorImage: Images.Vector, - ApiKey: "api-key", - // Defaults to bigquery for backwards compatibility with existing config.toml - Backend: LogflareBigQuery, - }, - EdgeRuntime: edgeRuntime{ - Image: Images.EdgeRuntime, - }, - }} - for _, apply := range editors { - apply(&initial) - } - return initial -} - -func (c *config) AuthExternalURL() string { - return c.Auth.GetExternalURL(c.Api.ExternalUrl) -} - -var ( - //go:embed templates/certs/kong.local.crt - kongCert []byte - //go:embed templates/certs/kong.local.key - kongKey []byte - - //go:embed templates/config.toml - initConfigEmbed string - initConfigTemplate = template.Must(template.New("initConfig").Parse(initConfigEmbed)) - - invalidProjectId = regexp.MustCompile("[^a-zA-Z0-9_.-]+") - refPattern = regexp.MustCompile(`^[a-z]{20}$`) -) - -func (c *config) Eject(w io.Writer) error { - // Defaults to current directory name as project id - if len(c.ProjectId) == 0 { - cwd, err := os.Getwd() - if err != nil { - return errors.Errorf("failed to get working directory: %w", err) - } - c.ProjectId = filepath.Base(cwd) - } - c.ProjectId = sanitizeProjectId(c.ProjectId) - // TODO: templatize all fields eventually - if err := initConfigTemplate.Option("missingkey=error").Execute(w, c); err != nil { - return errors.Errorf("failed to initialise config: %w", err) - } - return nil -} - -// Loads custom config file to struct fields tagged with toml. -func (c *config) loadFromFile(filename string, fsys fs.FS) error { - v := viper.NewWithOptions( - viper.ExperimentalBindStruct(), - viper.EnvKeyReplacer(strings.NewReplacer(".", "_")), - ) - fileConfig := viper.New() - v.SetEnvPrefix("SUPABASE") - v.AutomaticEnv() - if err := c.mergeDefaultValues(v); err != nil { - return err - } else if err := mergeFileConfig(v, filename, fsys); err != nil { - return err - } else if err := mergeFileConfig(fileConfig, filename, fsys); err != nil { - return err - } - v = normalizeDeprecatedSMTPConfig(v, fileConfig) - v = normalizeDeprecatedOrioleDBConfig(v, fileConfig) - // Find [remotes.*] block to override base config - idToName := map[string]string{} - for name, remote := range v.GetStringMap("remotes") { - projectId := v.GetString(fmt.Sprintf("remotes.%s.project_id", name)) - // Track remote project_id to check for duplication - if other, exists := idToName[projectId]; exists { - return errors.Errorf("duplicate project_id for [remotes.%s] and %s", name, other) - } - idToName[projectId] = fmt.Sprintf("[remotes.%s]", name) - if projectId == c.ProjectId { - fmt.Fprintln(os.Stderr, "Loading config override:", idToName[projectId]) - if err := mergeRemoteConfig(v, remote.(map[string]any)); err != nil { - return err - } - } - } - return c.load(v) -} - -func normalizeDeprecatedSMTPConfig(v, fileConfig *viper.Viper) *viper.Viper { - settings := v.AllSettings() - changed := false - if fileConfig.IsSet("inbucket") { - fmt.Fprintln(os.Stderr, `WARN: config section [inbucket] is deprecated. Please use [local_smtp] instead.`) - renameDeprecatedSMTP(settings, !fileConfig.IsSet("local_smtp")) - changed = true - } - if remotes, ok := settings["remotes"].(map[string]any); ok { - for name, raw := range remotes { - remote, ok := raw.(map[string]any) - if !ok || !fileConfig.IsSet(fmt.Sprintf("remotes.%s.inbucket", name)) { - continue - } - fmt.Fprintf( - os.Stderr, - "WARN: config section [remotes.%s.inbucket] is deprecated. Please use [remotes.%s.local_smtp] instead.\n", - name, - name, - ) - renameDeprecatedSMTP(remote, !fileConfig.IsSet(fmt.Sprintf("remotes.%s.local_smtp", name))) - changed = true - } - } - if !changed { - return v - } - return rebuildViperFromSettings(settings, v) -} - -// rebuildViperFromSettings rebuilds a viper instance from rewritten settings so a -// now-removed deprecated key does not trip UnmarshalExact. It preserves the -// env-binding options from the original instance, otherwise SUPABASE_-prefixed -// env overrides bound via ExperimentalBindStruct would be silently dropped. -func rebuildViperFromSettings(settings map[string]any, original *viper.Viper) *viper.Viper { - u := viper.NewWithOptions( - viper.ExperimentalBindStruct(), - viper.EnvKeyReplacer(strings.NewReplacer(".", "_")), - ) - u.SetEnvPrefix("SUPABASE") - u.AutomaticEnv() - if err := u.MergeConfigMap(settings); err != nil { - return original - } - return u -} - -func normalizeDeprecatedOrioleDBConfig(v, fileConfig *viper.Viper) *viper.Viper { - settings := v.AllSettings() - changed := promoteDeprecatedOrioleDBVersion(settings, fileConfig, "", "experimental.orioledb_version", "db.orioledb_version") - if remotes, ok := settings["remotes"].(map[string]any); ok { - for name, raw := range remotes { - remote, ok := raw.(map[string]any) - if !ok { - continue - } - legacyKey := fmt.Sprintf("remotes.%s.experimental.orioledb_version", name) - dbKey := fmt.Sprintf("remotes.%s.db.orioledb_version", name) - if promoteDeprecatedOrioleDBVersion(remote, fileConfig, name, legacyKey, dbKey) { - changed = true - } - } - } - if !changed { - return v - } - return rebuildViperFromSettings(settings, v) -} - -// promoteDeprecatedOrioleDBVersion promotes a non-empty legacy `experimental.orioledb_version` -// onto `db.orioledb_version` in settings when the latter is absent or empty in the user's -// file (the template always writes `db.orioledb_version = ""`, so an unset value is -// indistinguishable from an explicit empty string), and warns on stderr whenever the legacy -// value is non-empty. remoteName is empty for the top-level config, or a `[remotes.*]` name -// for the warning message. -func promoteDeprecatedOrioleDBVersion(settings map[string]any, fileConfig *viper.Viper, remoteName, legacyKey, dbKey string) bool { - legacyVal := fileConfig.GetString(legacyKey) - if legacyVal == "" { - return false - } - if fileConfig.GetString(dbKey) == "" { - db, ok := settings["db"].(map[string]any) - if !ok { - db = map[string]any{} - settings["db"] = db - } - db["orioledb_version"] = legacyVal - } - if remoteName == "" { - fmt.Fprintln(os.Stderr, "WARN: experimental.orioledb_version is deprecated. Please use db.orioledb_version instead.") - } else { - fmt.Fprintf( - os.Stderr, - "WARN: remotes.%s.experimental.orioledb_version is deprecated. Please use remotes.%s.db.orioledb_version instead.\n", - remoteName, - remoteName, - ) - } - return true -} - -// renameDeprecatedSMTP removes the deprecated `inbucket` key from settings. When -// promote is true (no explicit `local_smtp` is present), the inbucket values are -// deep-merged over any existing `local_smtp` defaults so a partial `[inbucket]` -// section keeps the template defaults it omits (e.g. `enabled = true`). -func renameDeprecatedSMTP(settings map[string]any, promote bool) { - inbucket, ok := settings["inbucket"] - delete(settings, "inbucket") - if !ok || !promote { - return - } - if existing, ok := settings["local_smtp"].(map[string]any); ok { - if override, ok := inbucket.(map[string]any); ok { - mergeConfigMaps(existing, override) - return - } - } - settings["local_smtp"] = inbucket -} - -// mergeConfigMaps deep-merges src into dst, overwriting leaf values while -// recursing into nested maps, mirroring viper's own config merge semantics. -func mergeConfigMaps(dst, src map[string]any) { - for k, val := range src { - if srcMap, ok := val.(map[string]any); ok { - if dstMap, ok := dst[k].(map[string]any); ok { - mergeConfigMaps(dstMap, srcMap) - continue - } - } - dst[k] = val - } -} - -func (c *config) mergeDefaultValues(v *viper.Viper) error { - v.SetConfigType("toml") - var buf bytes.Buffer - if err := c.Eject(&buf); err != nil { - return err - } else if err := v.MergeConfig(&buf); err != nil { - return errors.Errorf("failed to merge default values: %w", err) - } - return nil -} - -func mergeFileConfig(v *viper.Viper, filename string, fsys fs.FS) error { - if ext := filepath.Ext(filename); len(ext) > 0 { - v.SetConfigType(ext[1:]) - } - f, err := fsys.Open(filename) - if errors.Is(err, os.ErrNotExist) { - return nil - } else if err != nil { - return errors.Errorf("failed to read file config: %w", err) - } - defer f.Close() - if err := v.MergeConfig(f); err != nil { - return errors.Errorf("failed to merge file config: %w", err) - } - return nil -} - -func mergeRemoteConfig(v *viper.Viper, remote map[string]any) error { - u := viper.New() - if err := u.MergeConfigMap(remote); err != nil { - return errors.Errorf("failed to merge remote config: %w", err) - } - for _, k := range u.AllKeys() { - v.Set(k, u.Get(k)) - } - if key := "db.seed.enabled"; !u.IsSet(key) { - v.Set(key, false) - } - return nil -} - -func (c *config) load(v *viper.Viper) error { - // Set default values for [functions.*] when config struct is empty - for key, value := range v.GetStringMap("functions") { - if _, ok := value.(map[string]any); !ok { - // Leave validation to decode hook - continue - } - if k := fmt.Sprintf("functions.%s.enabled", key); !v.IsSet(k) { - v.Set(k, true) - } - } - // Set default values when [auth.email.smtp] is defined - if smtp := v.GetStringMap("auth.email.smtp"); len(smtp) > 0 { - if _, exists := smtp["enabled"]; !exists { - v.Set("auth.email.smtp.enabled", true) - } - } - if err := v.UnmarshalExact(c, func(dc *mapstructure.DecoderConfig) { - dc.TagName = "json" - dc.Squash = true - dc.ZeroFields = true - dc.DecodeHook = c.newDecodeHook(LoadEnvHook, ValidateFunctionsHook) - }); err != nil { - return errors.Errorf("failed to parse config: %w", err) - } - // Manually parse config to map - c.Storage.AnalyticsBuckets.Buckets = map[string]struct{}{} - for key := range v.GetStringMap("storage.analytics.buckets") { - c.Storage.AnalyticsBuckets.Buckets[key] = struct{}{} - } - c.Storage.VectorBuckets.Buckets = map[string]struct{}{} - for key := range v.GetStringMap("storage.vector.buckets") { - c.Storage.VectorBuckets.Buckets[key] = struct{}{} - } - // Convert keys to upper case: https://github.com/spf13/viper/issues/1014 - secrets := make(SecretsConfig, len(c.EdgeRuntime.Secrets)) - for k, v := range c.EdgeRuntime.Secrets { - secrets[strings.ToUpper(k)] = v - } - c.EdgeRuntime.Secrets = secrets - return nil -} - -func (c *config) newDecodeHook(fs ...mapstructure.DecodeHookFunc) mapstructure.DecodeHookFunc { - fs = append(fs, - mapstructure.StringToTimeDurationHookFunc(), - mapstructure.StringToIPHookFunc(), - mapstructure.StringToSliceHookFunc(","), - mapstructure.TextUnmarshallerHookFunc(), - DecryptSecretHookFunc(c.ProjectId), - ) - return mapstructure.ComposeDecodeHookFunc(fs...) -} - -func (c *config) Load(path string, fsys fs.FS, overrides ...ConfigEditor) error { - builder := NewPathBuilder(path) - // Load secrets from .env file - if err := loadNestedEnv(builder.SupabaseDirPath); err != nil { - return err - } - if err := c.loadFromFile(builder.ConfigPath, fsys); err != nil { - return err - } - // TODO: move linked pooler connection string elsewhere - if connString, err := readLinkedPoolerUrl(fsys, builder.PoolerUrlPath); err == nil && len(connString) > 0 { - c.Db.Pooler.ConnectionString = string(connString) - } - if len(c.Api.ExternalUrl) == 0 { - // Update external api url - apiUrl := url.URL{Host: net.JoinHostPort(c.Hostname, - strconv.FormatUint(uint64(c.Api.Port), 10), - )} - if c.Api.Tls.Enabled { - apiUrl.Scheme = "https" - } else { - apiUrl.Scheme = "http" - } - c.Api.ExternalUrl = apiUrl.String() - } - if len(c.Auth.ExternalUrl) == 0 { - c.Auth.ExternalUrl = c.AuthExternalURL() - } - // Set default JWT issuer if not configured - if len(c.Auth.JwtIssuer) == 0 { - c.Auth.JwtIssuer = c.Auth.ExternalUrl - } - // Update image versions - switch c.Db.MajorVersion { - case 13: - c.Db.Image = Images.Pg15 - case 14: - c.Db.Image = Images.Pg14 - case 15: - c.Db.Image = Images.Pg15 - } - if c.Db.MajorVersion > 14 { - if version, err := readTempFile(fsys, builder.PostgresVersionPath); err == nil { - // Only replace image if postgres version is above 15.1.0.55 - if i := strings.IndexByte(c.Db.Image, ':'); VersionCompare(c.Db.Image[i+1:], "15.1.0.55") >= 0 { - c.Db.Image = replaceImageTag(Images.Pg, string(version)) - } - } - if version, err := readTempFile(fsys, builder.RestVersionPath); err == nil && len(version) > 0 { - c.Api.Image = replaceImageTag(Images.Postgrest, string(version)) - } - if version, err := readTempFile(fsys, builder.GotrueVersionPath); err == nil && len(version) > 0 { - c.Auth.Image = replaceImageTag(Images.Gotrue, string(version)) - } - } - if version, err := readTempFile(fsys, builder.StorageVersionPath); err == nil && len(version) > 0 { - c.Storage.Image = replaceImageTag(Images.Storage, string(version)) - } - if version, err := readTempFile(fsys, builder.StorageMigrationPath); err == nil && len(version) > 0 { - c.Storage.TargetMigration = strings.TrimSpace(string(version)) - } - if version, err := readTempFile(fsys, builder.EdgeRuntimeVersionPath); err == nil && len(version) > 0 { - c.EdgeRuntime.Image = replaceImageTag(Images.EdgeRuntime, string(version)) - } - if version, err := readTempFile(fsys, builder.PoolerVersionPath); err == nil && len(version) > 0 { - c.Db.Pooler.Image = replaceImageTag(Images.Supavisor, string(version)) - } - if version, err := readTempFile(fsys, builder.RealtimeVersionPath); err == nil && len(version) > 0 { - c.Realtime.Image = replaceImageTag(Images.Realtime, string(version)) - } - if version, err := readTempFile(fsys, builder.StudioVersionPath); err == nil && len(version) > 0 { - c.Studio.Image = replaceImageTag(Images.Studio, string(version)) - } - if version, err := readTempFile(fsys, builder.PgmetaVersionPath); err == nil && len(version) > 0 { - c.Studio.PgmetaImage = replaceImageTag(Images.Pgmeta, string(version)) - } - if version, err := readTempFile(fsys, builder.LogflareVersionPath); err == nil && len(version) > 0 { - c.Analytics.Image = replaceImageTag(Images.Logflare, string(version)) - } - // TODO: replace derived config resolution with viper decode hooks - if err := c.resolve(builder, fsys); err != nil { - return err - } - for _, apply := range overrides { - apply(c) - } - return c.Validate(fsys) -} - -func VersionCompare(a, b string) int { - var pA, pB string - if vA := strings.Split(a, "."); len(vA) > 3 { - a = strings.Join(vA[:3], ".") - pA = strings.TrimLeft(strings.Join(vA[3:], "."), "0") - } - if vB := strings.Split(b, "."); len(vB) > 3 { - b = strings.Join(vB[:3], ".") - pB = strings.TrimLeft(strings.Join(vB[3:], "."), "0") - } - if r := semver.Compare("v"+a, "v"+b); r != 0 { - return r - } - return semver.Compare("v"+pA, "v"+pB) -} - -func (c *baseConfig) resolve(builder pathBuilder, fsys fs.FS) error { - // Update content paths - for name, tmpl := range c.Auth.Email.Template { - // FIXME: only email template is relative to repo directory - cwd := filepath.Dir(builder.SupabaseDirPath) - if len(tmpl.ContentPath) > 0 && !filepath.IsAbs(tmpl.ContentPath) { - tmpl.ContentPath = filepath.Join(cwd, tmpl.ContentPath) - } - c.Auth.Email.Template[name] = tmpl - } - for name, tmpl := range c.Auth.Email.Notification { - if len(tmpl.ContentPath) > 0 && !filepath.IsAbs(tmpl.ContentPath) { - tmpl.ContentPath = filepath.Join(builder.SupabaseDirPath, tmpl.ContentPath) - } - c.Auth.Email.Notification[name] = tmpl - } - // Update fallback configs - for name, bucket := range c.Storage.Buckets { - if bucket.FileSizeLimit == 0 { - bucket.FileSizeLimit = c.Storage.FileSizeLimit - } - if len(bucket.ObjectsPath) > 0 && !filepath.IsAbs(bucket.ObjectsPath) { - bucket.ObjectsPath = filepath.Join(builder.SupabaseDirPath, bucket.ObjectsPath) - } - c.Storage.Buckets[name] = bucket - } - // Resolve signing keys path for cross-platform compatibility - if len(c.Auth.SigningKeysPath) > 0 && !filepath.IsAbs(c.Auth.SigningKeysPath) { - c.Auth.SigningKeysPath = filepath.Join(builder.SupabaseDirPath, c.Auth.SigningKeysPath) - } - // Resolve functions config - for slug, function := range c.Functions { - if len(function.Entrypoint) == 0 { - function.Entrypoint = filepath.Join(builder.FunctionsDir, slug, "index.ts") - } else if !filepath.IsAbs(function.Entrypoint) { - // Append supabase/ because paths in configs are specified relative to config.toml - function.Entrypoint = filepath.Join(builder.SupabaseDirPath, function.Entrypoint) - } - if len(function.ImportMap) == 0 { - functionDir := filepath.Dir(function.Entrypoint) - denoJsonPath := filepath.Join(functionDir, "deno.json") - denoJsoncPath := filepath.Join(functionDir, "deno.jsonc") - if _, err := fs.Stat(fsys, denoJsonPath); err == nil { - function.ImportMap = denoJsonPath - } else if _, err := fs.Stat(fsys, denoJsoncPath); err == nil { - function.ImportMap = denoJsoncPath - } - // Functions may not use import map so we don't set a default value - } else if !filepath.IsAbs(function.ImportMap) { - function.ImportMap = filepath.Join(builder.SupabaseDirPath, function.ImportMap) - } - for i, val := range function.StaticFiles { - if len(val) > 0 && !filepath.IsAbs(val) { - function.StaticFiles[i] = filepath.Join(builder.SupabaseDirPath, val) - } - } - c.Functions[slug] = function - } - // Resolve TLS config - if c.Api.Enabled && c.Api.Tls.Enabled { - if len(c.Api.Tls.CertPath) > 0 { - c.Api.Tls.CertPath = path.Join(builder.SupabaseDirPath, c.Api.Tls.CertPath) - } - if len(c.Api.Tls.KeyPath) > 0 { - c.Api.Tls.KeyPath = path.Join(builder.SupabaseDirPath, c.Api.Tls.KeyPath) - } - } - // Resolve database config - if c.Db.Seed.Enabled { - for i, pattern := range c.Db.Seed.SqlPaths { - if len(pattern) > 0 && !filepath.IsAbs(pattern) { - c.Db.Seed.SqlPaths[i] = path.Join(builder.SupabaseDirPath, pattern) - } - } - } - for i, pattern := range c.Db.Migrations.SchemaPaths { - if len(pattern) > 0 && !filepath.IsAbs(pattern) { - c.Db.Migrations.SchemaPaths[i] = path.Join(builder.SupabaseDirPath, pattern) - } - } - if c.Experimental.PgDelta != nil && - len(c.Experimental.PgDelta.DeclarativeSchemaPath) > 0 && - !filepath.IsAbs(c.Experimental.PgDelta.DeclarativeSchemaPath) { - c.Experimental.PgDelta.DeclarativeSchemaPath = path.Join(builder.SupabaseDirPath, c.Experimental.PgDelta.DeclarativeSchemaPath) - } - return nil -} - -func (c *config) Validate(fsys fs.FS) error { - if c.ProjectId == "" { - return errors.New("Missing required field in config: project_id") - } else if sanitized := sanitizeProjectId(c.ProjectId); sanitized != c.ProjectId { - fmt.Fprintln(os.Stderr, "WARN: project_id field in config is invalid. Auto-fixing to", sanitized) - c.ProjectId = sanitized - } - // Since remote config is merged to base, we only need to validate the project_id field. - for name, remote := range c.Remotes { - if !refPattern.MatchString(remote.ProjectId) { - return errors.Errorf("Invalid config for remotes.%s.project_id. Must be like: abcdefghijklmnopqrst", name) - } - } - // Validate api config - if c.Api.AutoExposeNewTables != nil && *c.Api.AutoExposeNewTables { - fmt.Fprintln(os.Stderr, "WARN: api.auto_expose_new_tables is deprecated and will be removed on 2026-10-30. Remove the field or set it to false to adopt the new default of revoking Data API privileges on new entities in the public schema.") - } - if c.Api.Enabled { - if c.Api.Port == 0 { - return errors.New("Missing required field in config: api.port") - } - if c.Api.Tls.Enabled { - var err error - if len(c.Api.Tls.CertPath) > 0 { - if len(c.Api.Tls.KeyPath) == 0 { - return errors.New("Missing required field in config: api.tls.key_path") - } - if c.Api.Tls.CertContent, err = fs.ReadFile(fsys, c.Api.Tls.CertPath); err != nil { - return errors.Errorf("failed to read TLS cert: %w", err) - } - } - if len(c.Api.Tls.KeyPath) > 0 { - if len(c.Api.Tls.CertPath) == 0 { - return errors.New("Missing required field in config: api.tls.cert_path") - } - if c.Api.Tls.KeyContent, err = fs.ReadFile(fsys, c.Api.Tls.KeyPath); err != nil { - return errors.Errorf("failed to read TLS key: %w", err) - } - } - } - } - // Validate db config - if c.Db.Port == 0 { - return errors.New("Missing required field in config: db.port") - } - switch c.Db.MajorVersion { - case 0: - return errors.New("Missing required field in config: db.major_version") - case 12: - return errors.New("Postgres version 12.x is unsupported. To use the CLI, either start a new project or follow project migration steps here: https://supabase.com/docs/guides/database#migrating-between-projects.") - case 13, 14: - case 15, 17: - if len(c.Db.OrioleDBVersion) > 0 { - if VersionCompare(c.Db.OrioleDBVersion, "15.1.1.13") > 0 { - c.Db.Image = fmt.Sprintf("supabase/postgres:%s-orioledb", c.Db.OrioleDBVersion) - } else { - c.Db.Image = "supabase/postgres:orioledb-" + c.Db.OrioleDBVersion - } - if err := assertEnvLoaded(c.Experimental.S3Host); err != nil { - return err - } - if err := assertEnvLoaded(c.Experimental.S3Region); err != nil { - return err - } - if err := assertEnvLoaded(c.Experimental.S3AccessKey); err != nil { - return err - } - if err := assertEnvLoaded(c.Experimental.S3SecretKey); err != nil { - return err - } - } - default: - return errors.Errorf("Failed reading config: Invalid %s: %v.", "db.major_version", c.Db.MajorVersion) - } - // Validate storage config - for name := range c.Storage.Buckets { - if err := ValidateBucketName(name); err != nil { - return err - } - } - // Validate studio config - if c.Studio.Enabled { - if c.Studio.Port == 0 { - return errors.New("Missing required field in config: studio.port") - } - if parsed, err := url.Parse(c.Studio.ApiUrl); err != nil { - return errors.Errorf("Invalid config for studio.api_url: %w", err) - } else if parsed.Host == "" || parsed.Host == c.Hostname { - c.Studio.ApiUrl = c.Api.ExternalUrl - } - } - // Validate smtp config - if c.Inbucket.Enabled { - if c.Inbucket.Port == 0 { - return errors.New("Missing required field in config: local_smtp.port") - } - } - // Validate auth config - if c.Auth.Enabled { - if c.Auth.SiteUrl == "" { - return errors.New("Missing required field in config: auth.site_url") - } - if err := assertEnvLoaded(c.Auth.SiteUrl); err != nil { - return err - } - for i, url := range c.Auth.AdditionalRedirectUrls { - if err := assertEnvLoaded(url); err != nil { - return errors.Errorf("Invalid config for auth.additional_redirect_urls[%d]: %v", i, err) - } - } - if c.Auth.Captcha != nil && c.Auth.Captcha.Enabled { - if len(c.Auth.Captcha.Provider) == 0 { - return errors.New("Missing required field in config: auth.captcha.provider") - } - if len(c.Auth.Captcha.Secret.Value) == 0 { - return errors.Errorf("Missing required field in config: auth.captcha.secret") - } - if err := assertEnvLoaded(c.Auth.Captcha.Secret.Value); err != nil { - return err - } - } - if len(c.Auth.SigningKeysPath) > 0 { - if f, err := fsys.Open(c.Auth.SigningKeysPath); err != nil { - return errors.Errorf("failed to read signing keys: %w", err) - } else if c.Auth.SigningKeys, err = fetcher.ParseJSON[[]JWK](f); err != nil { - return errors.Errorf("failed to decode signing keys: %w", err) - } - } - if c.Auth.Passkey != nil && c.Auth.Passkey.Enabled { - if c.Auth.Webauthn == nil { - return errors.New("Missing required config section: auth.webauthn (required when auth.passkey.enabled is true)") - } - if len(c.Auth.Webauthn.RpId) == 0 { - return errors.New("Missing required field in config: auth.webauthn.rp_id") - } - if len(c.Auth.Webauthn.RpOrigins) == 0 { - return errors.New("Missing required field in config: auth.webauthn.rp_origins") - } - if err := assertEnvLoaded(c.Auth.Webauthn.RpId); err != nil { - return errors.Errorf("Invalid config for auth.webauthn.rp_id: %v", err) - } - for i, origin := range c.Auth.Webauthn.RpOrigins { - if err := assertEnvLoaded(origin); err != nil { - return errors.Errorf("Invalid config for auth.webauthn.rp_origins[%d]: %v", i, err) - } - } - } - if err := c.Auth.Hook.validate(); err != nil { - return err - } - if err := c.Auth.MFA.validate(); err != nil { - return err - } - if err := c.Auth.Email.validate(fsys); err != nil { - return err - } - if err := c.Auth.Sms.validate(); err != nil { - return err - } - if err := c.Auth.External.validate(); err != nil { - return err - } - if err := c.Auth.ThirdParty.validate(); err != nil { - return err - } - } - if err := c.Auth.generateAPIKeys(); err != nil { - return err - } - // Validate functions config - for name := range c.Functions { - if err := ValidateFunctionSlug(name); err != nil { - return err - } - } - switch c.EdgeRuntime.DenoVersion { - case 0: - return errors.New("Missing required field in config: edge_runtime.deno_version") - case 1: - c.EdgeRuntime.Image = deno1 - case 2: - break - default: - return errors.Errorf("Failed reading config: Invalid %s: %v.", "edge_runtime.deno_version", c.EdgeRuntime.DenoVersion) - } - // Validate logflare config - if c.Analytics.Enabled { - if c.Analytics.Backend == LogflareBigQuery { - if len(c.Analytics.GcpProjectId) == 0 { - return errors.New("Missing required field in config: analytics.gcp_project_id") - } - if len(c.Analytics.GcpProjectNumber) == 0 { - return errors.New("Missing required field in config: analytics.gcp_project_number") - } - if len(c.Analytics.GcpJwtPath) == 0 { - return errors.New("Path to GCP Service Account Key must be provided in config, relative to config.toml: analytics.gcp_jwt_path") - } - } - } - if err := c.Experimental.validate(); err != nil { - return err - } - return nil -} - -func assertEnvLoaded(s string) error { - if matches := envPattern.FindStringSubmatch(s); len(matches) > 1 { - fmt.Fprintln(os.Stderr, "WARN: environment variable is unset:", matches[1]) - } - return nil -} - -func truncateText(text string, maxLen int) string { - if len(text) > maxLen { - return text[:maxLen] - } - return text -} - -const maxProjectIdLength = 40 - -func sanitizeProjectId(src string) string { - // A valid project ID must only contain alphanumeric and special characters _.- - sanitized := invalidProjectId.ReplaceAllString(src, "_") - // It must also start with an alphanumeric character - sanitized = strings.TrimLeft(sanitized, "_.-") - // Truncate sanitized ID to 40 characters since docker hostnames cannot exceed - // 63 characters, and we need to save space for padding supabase_*_edge_runtime. - return truncateText(sanitized, maxProjectIdLength) -} - -func loadNestedEnv(basePath string) error { - repoDir, err := os.Getwd() - if err != nil { - return errors.Errorf("failed to get repo directory: %w", err) - } - if !filepath.IsAbs(basePath) { - basePath = filepath.Join(repoDir, basePath) - } - env := os.Getenv("SUPABASE_ENV") - for cwd := basePath; cwd != filepath.Dir(repoDir); cwd = filepath.Dir(cwd) { - if err := os.Chdir(cwd); err != nil && !errors.Is(err, os.ErrNotExist) { - return errors.Errorf("failed to change directory: %w", err) - } - if err := loadDefaultEnv(env); err != nil { - return err - } - } - if err := os.Chdir(repoDir); err != nil { - return errors.Errorf("failed to restore directory: %w", err) - } - return nil -} - -func loadDefaultEnv(env string) error { - if env == "" { - env = "development" - } - filenames := []string{".env." + env + ".local"} - if env != "test" { - filenames = append(filenames, ".env.local") - } - filenames = append(filenames, ".env."+env, ".env") - for _, path := range filenames { - if err := loadEnvIfExists(path); err != nil { - return err - } - } - return nil -} - -func loadEnvIfExists(path string) error { - if err := godotenv.Load(path); err != nil && !errors.Is(err, os.ErrNotExist) { - // If DEBUG=1, return the error as is for full debugability - if viper.GetBool("DEBUG") { - return errors.Errorf("failed to load %s: %w", path, err) - } - msg := err.Error() - switch { - case strings.HasPrefix(msg, "unexpected character"): - // Try to extract the character, fallback to generic - start := strings.Index(msg, "unexpected character \"") - if start != -1 { - start += len("unexpected character \"") - end := strings.Index(msg[start:], "\"") - if end != -1 { - char := msg[start : start+end] - return errors.Errorf("failed to parse environment file: %s (unexpected character '%s' in variable name)", path, char) - } - } - return errors.Errorf("failed to parse environment file: %s (unexpected character in variable name)", path) - case strings.HasPrefix(msg, "unterminated quoted value"): - return errors.Errorf("failed to parse environment file: %s (unterminated quoted value)", path) - // If the error message contains newlines, there is a high chance that the actual content of the - // dotenv file is being leaked. In such cases, we return a generic error to avoid unwanted leaks in the logs - case strings.Contains(msg, "\n"): - return errors.Errorf("failed to parse environment file: %s (syntax error)", path) - default: - return errors.Errorf("failed to load %s: %w", path, err) - } - } - return nil -} - -func (e *email) validate(fsys fs.FS) (err error) { - for name, tmpl := range e.Template { - if len(tmpl.ContentPath) == 0 { - if tmpl.Content != nil { - return errors.Errorf("Invalid config for auth.email.template.%s.content: please use content_path instead", name) - } - continue - } - if content, err := fs.ReadFile(fsys, tmpl.ContentPath); err != nil { - return errors.Errorf("Invalid config for auth.email.template.%s.content_path: %w", name, err) - } else { - tmpl.Content = cast.Ptr(string(content)) - } - e.Template[name] = tmpl - } - for name, tmpl := range e.Notification { - if !tmpl.Enabled { - continue - } - if len(tmpl.ContentPath) == 0 { - if tmpl.Content != nil { - return errors.Errorf("Invalid config for auth.email.notification.%s.content: please use content_path instead", name) - } - continue - } - if content, err := fs.ReadFile(fsys, tmpl.ContentPath); err != nil { - return errors.Errorf("Invalid config for auth.email.notification.%s.content_path: %w", name, err) - } else { - tmpl.Content = cast.Ptr(string(content)) - } - e.Notification[name] = tmpl - } - if e.Smtp != nil && e.Smtp.Enabled { - if len(e.Smtp.Host) == 0 { - return errors.New("Missing required field in config: auth.email.smtp.host") - } - if e.Smtp.Port == 0 { - return errors.New("Missing required field in config: auth.email.smtp.port") - } - if len(e.Smtp.User) == 0 { - return errors.New("Missing required field in config: auth.email.smtp.user") - } - if len(e.Smtp.Pass.Value) == 0 { - return errors.New("Missing required field in config: auth.email.smtp.pass") - } - if len(e.Smtp.AdminEmail) == 0 { - return errors.New("Missing required field in config: auth.email.smtp.admin_email") - } - if err := assertEnvLoaded(e.Smtp.Pass.Value); err != nil { - return err - } - } - return nil -} - -func (s *sms) validate() (err error) { - switch { - case s.Twilio.Enabled: - if len(s.Twilio.AccountSid) == 0 { - return errors.New("Missing required field in config: auth.sms.twilio.account_sid") - } - if len(s.Twilio.MessageServiceSid) == 0 { - return errors.New("Missing required field in config: auth.sms.twilio.message_service_sid") - } - if len(s.Twilio.AuthToken.Value) == 0 { - return errors.New("Missing required field in config: auth.sms.twilio.auth_token") - } - if err := assertEnvLoaded(s.Twilio.AuthToken.Value); err != nil { - return err - } - case s.TwilioVerify.Enabled: - if len(s.TwilioVerify.AccountSid) == 0 { - return errors.New("Missing required field in config: auth.sms.twilio_verify.account_sid") - } - if len(s.TwilioVerify.MessageServiceSid) == 0 { - return errors.New("Missing required field in config: auth.sms.twilio_verify.message_service_sid") - } - if len(s.TwilioVerify.AuthToken.Value) == 0 { - return errors.New("Missing required field in config: auth.sms.twilio_verify.auth_token") - } - if err := assertEnvLoaded(s.TwilioVerify.AuthToken.Value); err != nil { - return err - } - case s.Messagebird.Enabled: - if len(s.Messagebird.Originator) == 0 { - return errors.New("Missing required field in config: auth.sms.messagebird.originator") - } - if len(s.Messagebird.AccessKey.Value) == 0 { - return errors.New("Missing required field in config: auth.sms.messagebird.access_key") - } - if err := assertEnvLoaded(s.Messagebird.AccessKey.Value); err != nil { - return err - } - case s.Textlocal.Enabled: - if len(s.Textlocal.Sender) == 0 { - return errors.New("Missing required field in config: auth.sms.textlocal.sender") - } - if len(s.Textlocal.ApiKey.Value) == 0 { - return errors.New("Missing required field in config: auth.sms.textlocal.api_key") - } - if err := assertEnvLoaded(s.Textlocal.ApiKey.Value); err != nil { - return err - } - case s.Vonage.Enabled: - if len(s.Vonage.From) == 0 { - return errors.New("Missing required field in config: auth.sms.vonage.from") - } - if len(s.Vonage.ApiKey) == 0 { - return errors.New("Missing required field in config: auth.sms.vonage.api_key") - } - if len(s.Vonage.ApiSecret.Value) == 0 { - return errors.New("Missing required field in config: auth.sms.vonage.api_secret") - } - if err := assertEnvLoaded(s.Vonage.ApiKey); err != nil { - return err - } - if err := assertEnvLoaded(s.Vonage.ApiSecret.Value); err != nil { - return err - } - case s.EnableSignup: - s.EnableSignup = false - fmt.Fprintln(os.Stderr, "WARN: no SMS provider is enabled. Disabling phone login") - } - return nil -} - -func (e external) validate() (err error) { - for _, ext := range []string{"linkedin", "slack"} { - if e[ext].Enabled { - fmt.Fprintf(os.Stderr, `WARN: disabling deprecated "%[1]s" provider. Please use [auth.external.%[1]s_oidc] instead\n`, ext) - } - delete(e, ext) - } - for ext, provider := range e { - if !provider.Enabled { - continue - } - if provider.ClientId == "" { - return errors.Errorf("Missing required field in config: auth.external.%s.client_id", ext) - } - if !slices.Contains([]string{"apple", "google"}, ext) && len(provider.Secret.Value) == 0 { - return errors.Errorf("Missing required field in config: auth.external.%s.secret", ext) - } - if err := assertEnvLoaded(provider.ClientId); err != nil { - return err - } - if err := assertEnvLoaded(provider.Secret.Value); err != nil { - return err - } - if err := assertEnvLoaded(provider.RedirectUri); err != nil { - return err - } - if err := assertEnvLoaded(provider.Url); err != nil { - return err - } - e[ext] = provider - } - return nil -} - -func (h *hook) validate() error { - if hook := h.MFAVerificationAttempt; hook != nil { - if err := hook.validate("mfa_verification_attempt"); err != nil { - return err - } - } - if hook := h.PasswordVerificationAttempt; hook != nil { - if err := hook.validate("password_verification_attempt"); err != nil { - return err - } - } - if hook := h.CustomAccessToken; hook != nil { - if err := hook.validate("custom_access_token"); err != nil { - return err - } - } - if hook := h.SendSMS; hook != nil { - if err := hook.validate("send_sms"); err != nil { - return err - } - } - if hook := h.SendEmail; hook != nil { - if err := h.SendEmail.validate("send_email"); err != nil { - return err - } - } - if hook := h.BeforeUserCreated; hook != nil { - if err := hook.validate("before_user_created"); err != nil { - return err - } - } - return nil -} - -var hookSecretPattern = regexp.MustCompile(`^v1,whsec_[A-Za-z0-9+/=]{32,88}$`) - -func (h *hookConfig) validate(hookType string) (err error) { - // If not enabled do nothing - if !h.Enabled { - return nil - } - if h.URI == "" { - return errors.Errorf("Missing required field in config: auth.hook.%s.uri", hookType) - } - parsed, err := url.Parse(h.URI) - if err != nil { - return errors.Errorf("failed to parse template url: %w", err) - } - switch strings.ToLower(parsed.Scheme) { - case "http", "https": - if len(h.Secrets.Value) == 0 { - return errors.Errorf("Missing required field in config: auth.hook.%s.secrets", hookType) - } else if err := assertEnvLoaded(h.Secrets.Value); err != nil { - return err - } - for secret := range strings.SplitSeq(h.Secrets.Value, "|") { - if !hookSecretPattern.MatchString(secret) { - return errors.Errorf(`Invalid hook config: auth.hook.%s.secrets must be formatted as "v1,whsec_" with a minimum length of 32 characters.`, hookType) - } - } - case "pg-functions": - if len(h.Secrets.Value) > 0 { - return errors.Errorf("Invalid hook config: auth.hook.%s.secrets is unsupported for pg-functions URI", hookType) - } - default: - return errors.Errorf("Invalid hook config: auth.hook.%s.uri should be a HTTP, HTTPS, or pg-functions URI", hookType) - } - return nil -} - -func (m *mfa) validate() error { - if m.TOTP.EnrollEnabled && !m.TOTP.VerifyEnabled { - return errors.Errorf("Invalid MFA config: auth.mfa.totp.enroll_enabled requires verify_enabled") - } - if m.Phone.EnrollEnabled && !m.Phone.VerifyEnabled { - return errors.Errorf("Invalid MFA config: auth.mfa.phone.enroll_enabled requires verify_enabled") - } - if m.WebAuthn.EnrollEnabled && !m.WebAuthn.VerifyEnabled { - return errors.Errorf("Invalid MFA config: auth.mfa.web_authn.enroll_enabled requires verify_enabled") - } - return nil -} - -// TODO: use field tag validator instead -var funcSlugPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]*$`) - -func ValidateFunctionSlug(slug string) error { - if !funcSlugPattern.MatchString(slug) { - return errors.Errorf(`Invalid Function name: %s. Must start with at least one letter, and only include alphanumeric characters, underscores, and hyphens. (%s)`, slug, funcSlugPattern.String()) - } - return nil -} - -// Ref: https://github.com/supabase/storage/blob/master/src/storage/limits.ts#L59 -var bucketNamePattern = regexp.MustCompile(`^(\w|!|-|\.|\*|'|\(|\)| |&|\$|@|=|;|:|\+|,|\?)*$`) - -func ValidateBucketName(name string) error { - if !bucketNamePattern.MatchString(name) { - return errors.Errorf("Invalid Bucket name: %s. Only lowercase letters, numbers, dots, hyphens, and spaces are allowed. (%s)", name, bucketNamePattern.String()) - } - return nil -} - -func (f *tpaFirebase) issuerURL() string { - return fmt.Sprintf("https://securetoken.google.com/%s", f.ProjectID) -} - -func (f *tpaFirebase) validate() error { - if f.ProjectID == "" { - return errors.New("Invalid config: auth.third_party.firebase is enabled but without a project_id.") - } - - return nil -} - -func (a *tpaAuth0) issuerURL() string { - if a.TenantRegion != "" { - return fmt.Sprintf("https://%s.%s.auth0.com", a.Tenant, a.TenantRegion) - } - - return fmt.Sprintf("https://%s.auth0.com", a.Tenant) -} - -func (a *tpaAuth0) validate() error { - if a.Tenant == "" { - return errors.New("Invalid config: auth.third_party.auth0 is enabled but without a tenant.") - } - - return nil -} - -func (c *tpaCognito) issuerURL() string { - return fmt.Sprintf("https://cognito-idp.%s.amazonaws.com/%s", c.UserPoolRegion, c.UserPoolID) -} - -func (c *tpaCognito) validate() (err error) { - if c.UserPoolID == "" { - return errors.New("Invalid config: auth.third_party.cognito is enabled but without a user_pool_id.") - } else if err := assertEnvLoaded(c.UserPoolID); err != nil { - return err - } - - if c.UserPoolRegion == "" { - return errors.New("Invalid config: auth.third_party.cognito is enabled but without a user_pool_region.") - } else if err := assertEnvLoaded(c.UserPoolRegion); err != nil { - return err - } - - return nil -} - -var clerkDomainPattern = regexp.MustCompile("^(clerk([.][a-z0-9-]+){2,}|([a-z0-9-]+[.])+clerk[.]accounts[.]dev)$") - -func (c *tpaClerk) issuerURL() string { - return fmt.Sprintf("https://%s", c.Domain) -} - -func (c *tpaClerk) validate() (err error) { - if c.Domain == "" { - return errors.New("Invalid config: auth.third_party.clerk is enabled but without a domain.") - } else if err := assertEnvLoaded(c.Domain); err != nil { - return err - } - - if !clerkDomainPattern.MatchString(c.Domain) { - return errors.New("Invalid config: auth.third_party.clerk has invalid domain, it usually is like clerk.example.com or example.clerk.accounts.dev. Check https://clerk.com/setup/supabase on how to find the correct value.") - } - - return nil -} - -func (w *tpaWorkOs) validate() error { - if w.IssuerUrl == "" { - return errors.New("Invalid config: auth.third_party.workos is enabled but without a issuer_url.") - } - return nil -} - -func (w *tpaWorkOs) issuerURL() string { - return w.IssuerUrl -} - -func (tpa *thirdParty) validate() error { - enabled := 0 - - if tpa.Firebase.Enabled { - enabled += 1 - - if err := tpa.Firebase.validate(); err != nil { - return err - } - } - - if tpa.Auth0.Enabled { - enabled += 1 - - if err := tpa.Auth0.validate(); err != nil { - return err - } - } - - if tpa.Cognito.Enabled { - enabled += 1 - - if err := tpa.Cognito.validate(); err != nil { - return err - } - } - - if tpa.Clerk.Enabled { - enabled += 1 - - if err := tpa.Clerk.validate(); err != nil { - return err - } - } - - if tpa.WorkOs.Enabled { - enabled += 1 - - if err := tpa.WorkOs.validate(); err != nil { - return err - } - } - - if enabled > 1 { - return errors.New("Invalid config: Only one third_party provider allowed to be enabled at a time.") - } - - return nil -} - -func (tpa *thirdParty) IssuerURL() string { - if tpa.Firebase.Enabled { - return tpa.Firebase.issuerURL() - } - - if tpa.Auth0.Enabled { - return tpa.Auth0.issuerURL() - } - - if tpa.Cognito.Enabled { - return tpa.Cognito.issuerURL() - } - - if tpa.Clerk.Enabled { - return tpa.Clerk.issuerURL() - } - - if tpa.WorkOs.Enabled { - return tpa.WorkOs.issuerURL() - } - - return "" -} - -type ( - remoteJWKS struct { - Keys []json.RawMessage `json:"keys"` - } - - oidcConfiguration struct { - JWKSURI string `json:"jwks_uri"` - } - - secretJWK struct { - KeyType string `json:"kty"` - KeyBase64URL string `json:"k"` - } -) - -// ResolveJWKS creates the JWKS from the JWT secret and Third-Party Auth -// configs by resolving the JWKS via the OIDC discovery URL. -// It always returns a JWKS string, except when there's an error fetching. -func (a *auth) ResolveJWKS(ctx context.Context) (string, error) { - var jwks remoteJWKS - - if issuerURL := a.ThirdParty.IssuerURL(); issuerURL != "" { - discoveryURL := issuerURL + "/.well-known/openid-configuration" - - t := &http.Client{Timeout: 10 * time.Second} - client := fetcher.NewFetcher( - discoveryURL, - fetcher.WithHTTPClient(t), - fetcher.WithExpectedStatus(http.StatusOK), - ) - - resp, err := client.Send(ctx, http.MethodGet, "", nil) - if err != nil { - return "", err - } - - oidcConfig, err := fetcher.ParseJSON[oidcConfiguration](resp.Body) - if err != nil { - return "", err - } - - if oidcConfig.JWKSURI == "" { - return "", fmt.Errorf("auth.third_party: OIDC configuration at URL %q does not expose a jwks_uri property", discoveryURL) - } - - client = fetcher.NewFetcher( - oidcConfig.JWKSURI, - fetcher.WithHTTPClient(t), - fetcher.WithExpectedStatus(http.StatusOK), - ) - - resp, err = client.Send(ctx, http.MethodGet, "", nil) - if err != nil { - return "", err - } - - rJWKS, err := fetcher.ParseJSON[remoteJWKS](resp.Body) - if err != nil { - return "", err - } - - if len(rJWKS.Keys) == 0 { - return "", fmt.Errorf("auth.third_party: JWKS at URL %q as discovered from %q does not contain any JWK keys", oidcConfig.JWKSURI, discoveryURL) - } - - jwks.Keys = append(jwks.Keys, rJWKS.Keys...) - } - - // Convert each signing key to public-only version - for _, key := range a.SigningKeys { - publicKeyEncoded, err := json.Marshal(key.ToPublicJWK()) - if err != nil { - return "", errors.Errorf("failed to marshal public key: %w", err) - } - jwks.Keys = append(jwks.Keys, json.RawMessage(publicKeyEncoded)) - } - // Fallback to JWT_SECRET for backward compatibility - if len(a.SigningKeysPath) == 0 { - jwtSecret := secretJWK{ - KeyType: "oct", - KeyBase64URL: base64.RawURLEncoding.EncodeToString([]byte(a.JwtSecret.Value)), - } - - secretJWKEncoded, err := json.Marshal(&jwtSecret) - if err != nil { - return "", errors.Errorf("failed to marshal secret jwk: %w", err) - } - - jwks.Keys = append(jwks.Keys, json.RawMessage(secretJWKEncoded)) - } - - jwksEncoded, err := json.Marshal(jwks) - if err != nil { - return "", errors.Errorf("failed to marshal jwks keys: %w", err) - } - - return string(jwksEncoded), nil -} - -func (c *baseConfig) GetServiceImages() []string { - return []string{ - c.Db.Image, - c.Auth.Image, - c.Api.Image, - c.Realtime.Image, - c.Storage.Image, - c.EdgeRuntime.Image, - c.Studio.Image, - c.Studio.PgmetaImage, - c.Analytics.Image, - c.Db.Pooler.Image, - } -} - -// Retrieve the final base config to use taking into account the remotes override -// Pre: config must be loaded after setting config.ProjectID = "ref" -func (c *config) GetRemoteByProjectRef(projectRef string) (baseConfig, error) { - base := c.Clone() - for _, remote := range c.Remotes { - if remote.ProjectId == projectRef { - base.ProjectId = projectRef - return base, nil - } - } - return base, errors.Errorf("no remote found for project_id: %s", projectRef) -} - -func ToTomlBytes(config any) ([]byte, error) { - var buf bytes.Buffer - enc := toml.NewEncoder(&buf) - enc.Indent = "" - if err := enc.Encode(config); err != nil { - return nil, errors.Errorf("failed to marshal toml config: %w", err) - } - return buf.Bytes(), nil -} - -func (e *experimental) validate() error { - if e.Webhooks != nil && !e.Webhooks.Enabled { - return errors.Errorf("Webhooks cannot be deactivated. [experimental.webhooks] enabled can either be true or left undefined") - } - if e.PgDelta != nil && len(e.PgDelta.FormatOptions) > 0 && !json.Valid([]byte(e.PgDelta.FormatOptions)) { - return errors.Errorf("Invalid config for experimental.pgdelta.format_options: must be valid JSON") - } - return nil -} diff --git a/apps/cli-go/pkg/config/config_test.go b/apps/cli-go/pkg/config/config_test.go deleted file mode 100644 index 6ddd051a53..0000000000 --- a/apps/cli-go/pkg/config/config_test.go +++ /dev/null @@ -1,1327 +0,0 @@ -package config - -import ( - "bytes" - _ "embed" - "fmt" - "io" - "os" - "path" - "strings" - "testing" - fs "testing/fstest" - - "github.com/BurntSushi/toml" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -//go:embed testdata/config.toml -var testInitConfigEmbed []byte - -func TestConfigParsing(t *testing.T) { - t.Run("classic config file", func(t *testing.T) { - config := NewConfig() - // Run test - var buf bytes.Buffer - require.NoError(t, config.Eject(&buf)) - file := fs.MapFile{Data: buf.Bytes()} - fsys := fs.MapFS{"config.toml": &file} - // Check error - assert.NoError(t, config.Load("config.toml", fsys)) - }) - - t.Run("optional config file", func(t *testing.T) { - config := NewConfig() - // Run test - err := config.Load("", fs.MapFS{}) - // Check error - assert.NoError(t, err) - assert.True(t, config.Storage.VectorBuckets.Enabled) - }) - - t.Run("auth external url defaults from api external url", func(t *testing.T) { - config := NewConfig() - require.NoError(t, config.Load("", fs.MapFS{})) - - assert.Equal(t, strings.TrimRight(config.Api.ExternalUrl, "/")+"/auth/v1", config.Auth.ExternalUrl) - assert.Equal(t, config.Auth.ExternalUrl, config.Auth.JwtIssuer) - }) - - t.Run("auth external url and jwt issuer preserve explicit overrides", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "config.toml": &fs.MapFile{Data: []byte(` -[api] -external_url = "https://api.example.com/" - -[auth] -site_url = "https://app.example.com" -external_url = "https://auth.example.com/custom/" -jwt_issuer = "https://issuer.example.com/custom/" -`)}, - } - - require.NoError(t, config.Load("config.toml", fsys)) - assert.Equal(t, "https://auth.example.com/custom/", config.Auth.ExternalUrl) - assert.Equal(t, "https://issuer.example.com/custom/", config.Auth.JwtIssuer) - }) - - t.Run("config file with environment variables", func(t *testing.T) { - config := NewConfig() - // Setup in-memory fs - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: testInitConfigEmbed}, - "supabase/templates/invite.html": &fs.MapFile{}, - "supabase/templates/password_changed_notification.html": &fs.MapFile{}, - "supabase/signing_keys.json": &fs.MapFile{Data: []byte("[]")}, - "certs/my-cert.pem": &fs.MapFile{}, - "certs/my-key.pem": &fs.MapFile{}, - } - // Run test - t.Setenv("TWILIO_AUTH_TOKEN", "token") - t.Setenv("AZURE_CLIENT_ID", "hello") - t.Setenv("AZURE_SECRET", "this is cool") - t.Setenv("AUTH_SEND_SMS_SECRETS", "v1,whsec_aWxpa2VzdXBhYmFzZXZlcnltdWNoYW5kaWhvcGV5b3Vkb3Rvbw==") - t.Setenv("SENDGRID_API_KEY", "sendgrid") - t.Setenv("AUTH_CALLBACK_URL", "http://localhost:3000/auth/callback") - assert.NoError(t, config.Load("", fsys)) - // Check error - assert.Equal(t, "hello", config.Auth.External["azure"].ClientId) - assert.Equal(t, "this is cool", config.Auth.External["azure"].Secret.Value) - assert.Equal(t, []string{ - "https://127.0.0.1:3000", - "http://localhost:3000/auth/callback", - }, config.Auth.AdditionalRedirectUrls) - }) - - t.Run("config file with environment variables fails when unset", func(t *testing.T) { - config := NewConfig() - // Setup in-memory fs - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: testInitConfigEmbed}, - } - // Run test - assert.Error(t, config.Load("", fsys)) - }) - t.Run("config file with passkey and webauthn settings", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` -[auth] -enabled = true -site_url = "http://127.0.0.1:3000" -[auth.passkey] -enabled = true -[auth.webauthn] -rp_display_name = "Supabase CLI" -rp_id = "localhost" -rp_origins = ["http://127.0.0.1:3000", "https://localhost:3000"] -`)}, - } - // Run test - assert.NoError(t, config.Load("", fsys)) - // Check result - if assert.NotNil(t, config.Auth.Passkey) { - assert.True(t, config.Auth.Passkey.Enabled) - } - if assert.NotNil(t, config.Auth.Webauthn) { - assert.Equal(t, "Supabase CLI", config.Auth.Webauthn.RpDisplayName) - assert.Equal(t, "localhost", config.Auth.Webauthn.RpId) - assert.Equal(t, []string{ - "http://127.0.0.1:3000", - "https://localhost:3000", - }, config.Auth.Webauthn.RpOrigins) - } - }) - - t.Run("webauthn section without passkey loads successfully", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` -[auth] -enabled = true -site_url = "http://127.0.0.1:3000" -[auth.webauthn] -rp_display_name = "Supabase CLI" -rp_id = "localhost" -rp_origins = ["http://127.0.0.1:3000"] -`)}, - } - // Run test - assert.NoError(t, config.Load("", fsys)) - // Check result - assert.Nil(t, config.Auth.Passkey) - if assert.NotNil(t, config.Auth.Webauthn) { - assert.Equal(t, "localhost", config.Auth.Webauthn.RpId) - } - }) - - t.Run("passkey enabled requires webauthn section", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` -[auth] -enabled = true -site_url = "http://127.0.0.1:3000" -[auth.passkey] -enabled = true -`)}, - } - // Run test - err := config.Load("", fsys) - // Check result - assert.ErrorContains(t, err, "Missing required config section: auth.webauthn") - }) - - t.Run("passkey enabled requires rp_id", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` -[auth] -enabled = true -site_url = "http://127.0.0.1:3000" -[auth.passkey] -enabled = true -[auth.webauthn] -rp_origins = ["http://127.0.0.1:3000"] -`)}, - } - // Run test - err := config.Load("", fsys) - // Check result - assert.ErrorContains(t, err, "Missing required field in config: auth.webauthn.rp_id") - }) - - t.Run("passkey enabled requires rp_origins", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` -[auth] -enabled = true -site_url = "http://127.0.0.1:3000" -[auth.passkey] -enabled = true -[auth.webauthn] -rp_id = "localhost" -`)}, - } - // Run test - err := config.Load("", fsys) - // Check result - assert.ErrorContains(t, err, "Missing required field in config: auth.webauthn.rp_origins") - }) - - t.Run("parses experimental pgdelta config", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` -[experimental.pgdelta] -enabled = true -declarative_schema_path = "./db/decl" -format_options = "{\"keywordCase\":\"upper\",\"indent\":2}" -`)}, - } - - require.NoError(t, config.Load("", fsys)) - require.NotNil(t, config.Experimental.PgDelta) - assert.True(t, config.Experimental.PgDelta.Enabled) - assert.Equal(t, path.Join("supabase", "db", "decl"), config.Experimental.PgDelta.DeclarativeSchemaPath) - assert.Equal(t, `{"keywordCase":"upper","indent":2}`, config.Experimental.PgDelta.FormatOptions) - }) - - t.Run("rejects invalid experimental pgdelta format options", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` -[experimental.pgdelta] -format_options = "not-json" -`)}, - } - - err := config.Load("", fsys) - assert.ErrorContains(t, err, "experimental.pgdelta.format_options") - }) - - t.Run("init scaffold opts into pgdelta", func(t *testing.T) { - config := NewConfig() - // supabase init renders the scaffold with the pg-delta opt-in flag set - config.Experimental.PgDeltaInitEnabled = true - var buf bytes.Buffer - require.NoError(t, config.Eject(&buf)) - fsys := fs.MapFS{"supabase/config.toml": &fs.MapFile{Data: buf.Bytes()}} - // Reloading the generated config resolves to pg-delta - require.NoError(t, config.Load("", fsys)) - require.NotNil(t, config.Experimental.PgDelta) - assert.True(t, config.Experimental.PgDelta.Enabled) - }) - - t.Run("absent pgdelta section falls back to migra", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` -[experimental] -orioledb_version = "" -`)}, - } - - // The default ejected by mergeDefaultValues keeps pg-delta disabled, so a config - // without the section resolves to migra (PgDelta is non-nil only for version pinning). - require.NoError(t, config.Load("", fsys)) - require.NotNil(t, config.Experimental.PgDelta) - assert.False(t, config.Experimental.PgDelta.Enabled) - }) - - t.Run("explicit enabled false restores migra", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` -[experimental.pgdelta] -enabled = false -`)}, - } - - require.NoError(t, config.Load("", fsys)) - require.NotNil(t, config.Experimental.PgDelta) - assert.False(t, config.Experimental.PgDelta.Enabled) - }) - - // [compute] is owned by the TS CLI, but the published JSON schema advertises it, - // so a user can hand-write it today. Every Go-delegated path goes through - // config.Load, and UnmarshalExact rejects keys baseConfig does not model — so the - // section has to at least parse here, in both base and remote position. - t.Run("accepts the TS-owned compute section", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` -project_id = "test" - -[compute.api] -runtime = "node" - -[remotes.prod] -project_id = "bvikqvbczudanvggcord" - -[remotes.prod.compute.api] -instances = 3 -`)}, - } - - assert.NoError(t, config.Load("", fsys)) - }) - - t.Run("accepts the TS-owned local section", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` -project_id = "test" - -[local.services] -default = { version = "bundled" } -auth = { version = "v2.197.0" } -`)}, - } - - assert.NoError(t, config.Load("", fsys)) - }) - - for _, tt := range []struct { - name string - configData string - projectID string - want bool - }{ - { - name: "base true", - configData: "[experimental]\nstack = true\n", - want: true, - }, - { - name: "base false", - configData: "[experimental]\nstack = false\n", - want: false, - }, - { - name: "remote true", - configData: "[remotes.prod]\nproject_id = \"abcdefghijklmnopqrst\"\n[remotes.prod.experimental]\nstack = true\n", - projectID: "abcdefghijklmnopqrst", - want: true, - }, - { - name: "remote false", - configData: "[remotes.prod]\nproject_id = \"abcdefghijklmnopqrst\"\n[remotes.prod.experimental]\nstack = false\n", - projectID: "abcdefghijklmnopqrst", - want: false, - }, - } { - t.Run("accepts experimental stack "+tt.name, func(t *testing.T) { - t.Setenv("SUPABASE_EXPERIMENTAL_STACK", "") - config := NewConfig() - config.ProjectId = tt.projectID - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(tt.configData)}, - } - - require.NoError(t, config.Load("", fsys)) - assert.Equal(t, tt.want, config.Experimental.Stack) - }) - } - - for _, tt := range []struct { - name string - configData string - projectID string - want bool - }{ - { - name: "base true", - configData: "[experimental]\ncompute = true\n", - want: true, - }, - { - name: "base false", - configData: "[experimental]\ncompute = false\n", - want: false, - }, - { - name: "remote true", - configData: "[remotes.prod]\nproject_id = \"abcdefghijklmnopqrst\"\n[remotes.prod.experimental]\ncompute = true\n", - projectID: "abcdefghijklmnopqrst", - want: true, - }, - { - name: "remote false", - configData: "[remotes.prod]\nproject_id = \"abcdefghijklmnopqrst\"\n[remotes.prod.experimental]\ncompute = false\n", - projectID: "abcdefghijklmnopqrst", - want: false, - }, - } { - t.Run("accepts experimental compute "+tt.name, func(t *testing.T) { - t.Setenv("SUPABASE_EXPERIMENTAL_COMPUTE", "") - config := NewConfig() - config.ProjectId = tt.projectID - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(tt.configData)}, - } - - require.NoError(t, config.Load("", fsys)) - assert.Equal(t, tt.want, config.Experimental.Compute) - }) - } - - t.Run("does not emit experimental stack or compute", func(t *testing.T) { - config := NewConfig() - config.Experimental.Stack = true - config.Experimental.Compute = true - - encodedToml, err := ToTomlBytes(config.Experimental) - require.NoError(t, err) - var encoded map[string]any - _, err = toml.Decode(string(encodedToml), &encoded) - require.NoError(t, err) - assert.NotContains(t, encoded, "stack") - assert.NotContains(t, encoded, "compute") - - var buf bytes.Buffer - require.NoError(t, config.Eject(&buf)) - var rendered map[string]any - _, err = toml.Decode(buf.String(), &rendered) - require.NoError(t, err) - experimental, ok := rendered["experimental"].(map[string]any) - if assert.True(t, ok) { - assert.NotContains(t, experimental, "stack") - assert.NotContains(t, experimental, "compute") - } - }) -} - -func TestRemoteOverride(t *testing.T) { - t.Run("load staging override", func(t *testing.T) { - config := NewConfig() - config.ProjectId = "bvikqvbczudanvggcord" - // Setup in-memory fs - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: testInitConfigEmbed}, - "supabase/templates/invite.html": &fs.MapFile{}, - "supabase/templates/password_changed_notification.html": &fs.MapFile{}, - "certs/my-cert.pem": &fs.MapFile{}, - "certs/my-key.pem": &fs.MapFile{}, - "supabase/signing_keys.json": &fs.MapFile{Data: []byte("[]")}, - } - // Run test - t.Setenv("SUPABASE_AUTH_SITE_URL", "http://preview.com") - t.Setenv("AUTH_SEND_SMS_SECRETS", "v1,whsec_aWxpa2VzdXBhYmFzZXZlcnltdWNoYW5kaWhvcGV5b3Vkb3Rvbw==") - assert.NoError(t, config.Load("", fsys)) - // Check error - assert.True(t, config.Db.Seed.Enabled) - assert.Equal(t, "http://preview.com", config.Auth.SiteUrl) - assert.Equal(t, []string{"image/png"}, config.Storage.Buckets["images"].AllowedMimeTypes) - }) - - t.Run("load production override", func(t *testing.T) { - config := NewConfig() - config.ProjectId = "vpefcjyosynxeiebfscx" - // Setup in-memory fs - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: testInitConfigEmbed}, - "supabase/templates/invite.html": &fs.MapFile{}, - "supabase/templates/password_changed_notification.html": &fs.MapFile{}, - "certs/my-cert.pem": &fs.MapFile{}, - "certs/my-key.pem": &fs.MapFile{}, - "supabase/signing_keys.json": &fs.MapFile{Data: []byte("[]")}, - } - // Run test - t.Setenv("SUPABASE_AUTH_SITE_URL", "http://preview.com") - t.Setenv("AUTH_SEND_SMS_SECRETS", "v1,whsec_aWxpa2VzdXBhYmFzZXZlcnltdWNoYW5kaWhvcGV5b3Vkb3Rvbw==") - assert.NoError(t, config.Load("", fsys)) - // Check error - assert.False(t, config.Db.Seed.Enabled) - assert.Equal(t, "http://feature-auth-branch.com/", config.Auth.SiteUrl) - assert.Equal(t, false, config.Auth.External["azure"].Enabled) - assert.Equal(t, "nope", config.Auth.External["azure"].ClientId) - }) - - t.Run("config file with remotes", func(t *testing.T) { - config := NewConfig() - // Setup in-memory fs - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: testInitConfigEmbed}, - "supabase/templates/invite.html": &fs.MapFile{}, - "supabase/templates/password_changed_notification.html": &fs.MapFile{}, - "certs/my-cert.pem": &fs.MapFile{}, - "certs/my-key.pem": &fs.MapFile{}, - "supabase/signing_keys.json": &fs.MapFile{Data: []byte("[]")}, - } - // Run test - t.Setenv("TWILIO_AUTH_TOKEN", "token") - t.Setenv("AZURE_CLIENT_ID", "hello") - t.Setenv("AZURE_SECRET", "this is cool") - t.Setenv("AUTH_SEND_SMS_SECRETS", "v1,whsec_aWxpa2VzdXBhYmFzZXZlcnltdWNoYW5kaWhvcGV5b3Vkb3Rvbw==") - t.Setenv("SENDGRID_API_KEY", "sendgrid") - t.Setenv("AUTH_CALLBACK_URL", "http://localhost:3000/auth/callback") - assert.NoError(t, config.Load("", fsys)) - // Check the default value in the config - assert.Equal(t, "http://127.0.0.1:3000", config.Auth.SiteUrl) - assert.Equal(t, true, config.Auth.EnableSignup) - assert.Equal(t, true, config.Auth.External["azure"].Enabled) - assert.Equal(t, []string{"image/png", "image/jpeg"}, config.Storage.Buckets["images"].AllowedMimeTypes) - // Check the values for remotes override - production, ok := config.Remotes["production"] - assert.True(t, ok) - staging, ok := config.Remotes["staging"] - assert.True(t, ok) - // Check the values for production override - assert.Equal(t, "vpefcjyosynxeiebfscx", production.ProjectId) - assert.Equal(t, "http://feature-auth-branch.com/", production.Auth.SiteUrl) - assert.Equal(t, false, production.Auth.EnableSignup) - assert.Equal(t, false, production.Auth.External["azure"].Enabled) - assert.Equal(t, "nope", production.Auth.External["azure"].ClientId) - // Check seed should be disabled by default for remote configs - assert.Equal(t, false, production.Db.Seed.Enabled) - // Check the values for the staging override - assert.Equal(t, "bvikqvbczudanvggcord", staging.ProjectId) - assert.Equal(t, []string{"image/png"}, staging.Storage.Buckets["images"].AllowedMimeTypes) - assert.Equal(t, true, staging.Db.Seed.Enabled) - }) -} - -func TestFileSizeLimitConfigParsing(t *testing.T) { - t.Run("test file size limit parsing number", func(t *testing.T) { - var testConfig config - _, err := toml.Decode(` - [storage] - file_size_limit = 5000000 - `, &testConfig) - if assert.NoError(t, err) { - assert.Equal(t, sizeInBytes(5000000), testConfig.Storage.FileSizeLimit) - } - }) - - t.Run("test file size limit parsing bytes unit", func(t *testing.T) { - var testConfig config - _, err := toml.Decode(` - [storage] - file_size_limit = "5MB" - `, &testConfig) - if assert.NoError(t, err) { - assert.Equal(t, sizeInBytes(5242880), testConfig.Storage.FileSizeLimit) - } - }) - - t.Run("test file size limit parsing binary bytes unit", func(t *testing.T) { - var testConfig config - _, err := toml.Decode(` - [storage] - file_size_limit = "5MiB" - `, &testConfig) - if assert.NoError(t, err) { - assert.Equal(t, sizeInBytes(5242880), testConfig.Storage.FileSizeLimit) - } - }) - - t.Run("test file size limit parsing string number", func(t *testing.T) { - var testConfig config - _, err := toml.Decode(` - [storage] - file_size_limit = "5000000" - `, &testConfig) - if assert.NoError(t, err) { - assert.Equal(t, sizeInBytes(5000000), testConfig.Storage.FileSizeLimit) - } - }) - - t.Run("test file size limit parsing bad datatype", func(t *testing.T) { - var testConfig config - _, err := toml.Decode(` - [storage] - file_size_limit = [] - `, &testConfig) - assert.Error(t, err) - assert.Equal(t, sizeInBytes(0), testConfig.Storage.FileSizeLimit) - }) - - t.Run("test file size limit parsing bad string data", func(t *testing.T) { - var testConfig config - _, err := toml.Decode(` - [storage] - file_size_limit = "foobar" - `, &testConfig) - assert.Error(t, err) - assert.Equal(t, sizeInBytes(0), testConfig.Storage.FileSizeLimit) - }) -} - -func TestSanitizeProjectI(t *testing.T) { - // Preserves valid consecutive characters - assert.Equal(t, "abc", sanitizeProjectId("abc")) - assert.Equal(t, "a..b_c", sanitizeProjectId("a..b_c")) - // Removes leading special characters - assert.Equal(t, "abc", sanitizeProjectId("_abc")) - assert.Equal(t, "abc", sanitizeProjectId("_@abc")) - // Replaces consecutive invalid characters with a single _ - assert.Equal(t, "a_bc-", sanitizeProjectId("a@@bc-")) - // Truncates to less than 40 characters - sanitized := strings.Repeat("a", maxProjectIdLength) - assert.Equal(t, sanitized, sanitizeProjectId(sanitized+"bb")) -} - -const ( - defaultAnonKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" - defaultServiceRoleKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU" -) - -func TestSigningJWT(t *testing.T) { - t.Run("signs default anon key", func(t *testing.T) { - anonToken := CustomClaims{Role: "anon"}.NewToken() - signed, err := anonToken.SignedString([]byte(defaultJwtSecret)) - assert.NoError(t, err) - assert.Equal(t, defaultAnonKey, signed) - }) - - t.Run("signs default service_role key", func(t *testing.T) { - serviceToken := CustomClaims{Role: "service_role"}.NewToken() - signed, err := serviceToken.SignedString([]byte(defaultJwtSecret)) - assert.NoError(t, err) - assert.Equal(t, defaultServiceRoleKey, signed) - }) -} - -func TestValidateHookURI(t *testing.T) { - tests := []struct { - hookConfig - name string - errorMsg string - }{ - { - name: "valid http URL", - hookConfig: hookConfig{ - Enabled: true, - URI: "http://example.com", - Secrets: Secret{Value: "v1,whsec_aWxpa2VzdXBhYmFzZXZlcnltdWNoYW5kaWhvcGV5b3Vkb3Rvbw=="}, - }, - }, - { - name: "valid https URL", - hookConfig: hookConfig{ - Enabled: true, - URI: "https://example.com", - Secrets: Secret{Value: "v1,whsec_aWxpa2VzdXBhYmFzZXZlcnltdWNoYW5kaWhvcGV5b3Vkb3Rvbw=="}, - }, - }, - { - name: "valid pg-functions URI", - hookConfig: hookConfig{ - Enabled: true, - URI: "pg-functions://functionName", - }, - }, - { - name: "invalid URI with unsupported scheme", - hookConfig: hookConfig{ - Enabled: true, - URI: "ftp://example.com", - Secrets: Secret{Value: "v1,whsec_aWxpa2VzdXBhYmFzZXZlcnltdWNoYW5kaWhvcGV5b3Vkb3Rvbw=="}, - }, - errorMsg: "Invalid hook config: auth.hook.invalid URI with unsupported scheme.uri should be a HTTP, HTTPS, or pg-functions URI", - }, - { - name: "invalid URI with parsing error", - hookConfig: hookConfig{ - Enabled: true, - URI: "http://a b.com", - Secrets: Secret{Value: "v1,whsec_aWxpa2VzdXBhYmFzZXZlcnltdWNoYW5kaWhvcGV5b3Vkb3Rvbw=="}, - }, - errorMsg: "failed to parse template url: parse \"http://a b.com\": invalid character \" \" in host name", - }, - { - name: "valid http URL with missing secrets", - hookConfig: hookConfig{ - Enabled: true, - URI: "http://example.com", - }, - errorMsg: "Missing required field in config: auth.hook.valid http URL with missing secrets.secrets", - }, - { - name: "valid pg-functions URI with unsupported secrets", - hookConfig: hookConfig{ - Enabled: true, - URI: "pg-functions://functionName", - Secrets: Secret{Value: "test-secret"}, - }, - errorMsg: "Invalid hook config: auth.hook.valid pg-functions URI with unsupported secrets.secrets is unsupported for pg-functions URI", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.validate(tt.name) - if len(tt.errorMsg) > 0 { - assert.Error(t, err, "Expected an error for %v", tt.name) - assert.EqualError(t, err, tt.errorMsg, "Expected error message does not match for %v", tt.name) - } else { - assert.NoError(t, err, "Expected no error for %v", tt.name) - } - }) - } -} - -func TestGlobFiles(t *testing.T) { - t.Run("returns seed files matching patterns", func(t *testing.T) { - // Setup in-memory fs - fsys := fs.MapFS{ - "supabase/seeds/seed1.sql": &fs.MapFile{Data: []byte("INSERT INTO table1 VALUES (1);")}, - "supabase/seeds/seed2.sql": &fs.MapFile{Data: []byte("INSERT INTO table2 VALUES (2);")}, - "supabase/seeds/seed3.sql": &fs.MapFile{Data: []byte("INSERT INTO table2 VALUES (2);")}, - "supabase/seeds/another.sql": &fs.MapFile{Data: []byte("INSERT INTO table2 VALUES (2);")}, - "supabase/seeds/ignore.sql": &fs.MapFile{Data: []byte("INSERT INTO table3 VALUES (3);")}, - } - // Mock config patterns - g := Glob{ - "supabase/seeds/seed[12].sql", - "supabase/seeds/ano*.sql", - } - // Run test - files, err := g.Files(fsys) - // Check error - assert.NoError(t, err) - // Validate files - assert.ElementsMatch(t, []string{ - "supabase/seeds/seed1.sql", - "supabase/seeds/seed2.sql", - "supabase/seeds/another.sql", - }, files) - }) - - t.Run("returns seed files matching patterns skip duplicates", func(t *testing.T) { - // Setup in-memory fs - fsys := fs.MapFS{ - "supabase/seeds/seed1.sql": &fs.MapFile{Data: []byte("INSERT INTO table1 VALUES (1);")}, - "supabase/seeds/seed2.sql": &fs.MapFile{Data: []byte("INSERT INTO table2 VALUES (2);")}, - "supabase/seeds/seed3.sql": &fs.MapFile{Data: []byte("INSERT INTO table2 VALUES (2);")}, - "supabase/seeds/another.sql": &fs.MapFile{Data: []byte("INSERT INTO table2 VALUES (2);")}, - "supabase/seeds/ignore.sql": &fs.MapFile{Data: []byte("INSERT INTO table3 VALUES (3);")}, - } - // Mock config patterns - g := Glob{ - "supabase/seeds/seed[12].sql", - "supabase/seeds/ano*.sql", - "supabase/seeds/seed*.sql", - } - // Run test - files, err := g.Files(fsys) - // Check error - assert.NoError(t, err) - // Validate files - assert.ElementsMatch(t, []string{ - "supabase/seeds/seed1.sql", - "supabase/seeds/seed2.sql", - "supabase/seeds/another.sql", - "supabase/seeds/seed3.sql", - }, files) - }) - - t.Run("returns error on invalid pattern", func(t *testing.T) { - // Setup in-memory fs - fsys := fs.MapFS{} - // Mock config patterns - g := Glob{"[*!#@D#"} - // Run test - files, err := g.Files(fsys) - // Check error - assert.ErrorIs(t, err, path.ErrBadPattern) - // The resuling seed list should be empty - assert.Empty(t, files) - }) - - t.Run("returns empty list if no files match", func(t *testing.T) { - // Setup in-memory fs - fsys := fs.MapFS{} - // Mock config patterns - g := Glob{"seeds/*.sql"} - // Run test - files, err := g.Files(fsys) - // Check error - assert.ErrorContains(t, err, "no files matched") - // Validate files - assert.Empty(t, files) - }) - - t.Run("skips empty globs when configured", func(t *testing.T) { - fsys := fs.MapFS{ - "supabase/schemas/tables/players.sql": &fs.MapFile{}, - } - g := Glob{ - "supabase/schemas/tables/*.sql", - "supabase/schemas/materialized_views/*.sql", - } - - files, err := g.Files(fsys, WithSkipEmptyGlobs()) - - assert.NoError(t, err) - assert.Equal(t, []string{"supabase/schemas/tables/players.sql"}, files) - }) - - t.Run("errors when all skipped globs are empty and configured to fail", func(t *testing.T) { - fsys := fs.MapFS{} - g := Glob{ - "supabase/schemas/tables/*.sql", - "supabase/schemas/materialized_views/*.sql", - } - - files, err := g.Files(fsys, WithSkipEmptyGlobs(), WithErrorOnAllSkippedGlobs()) - - assert.ErrorContains(t, err, "no files matched pattern") - assert.Empty(t, files) - }) -} - -func TestGlobSQLFiles(t *testing.T) { - t.Run("expands directory entries in declared order", func(t *testing.T) { - fsys := fs.MapFS{ - "supabase/schemas/z_function.sql": &fs.MapFile{Data: []byte("select 1;")}, - "supabase/schemas/tables/a_table.sql": &fs.MapFile{Data: []byte("select 2;")}, - "supabase/schemas/tables/nested/b_table.sql": &fs.MapFile{Data: []byte("select 3;")}, - "supabase/schemas/tables/readme.md": &fs.MapFile{Data: []byte("ignored")}, - } - g := Glob{ - "supabase/schemas/z_function.sql", - "supabase/schemas/tables", - } - - files, err := g.SQLFiles(fsys) - - assert.NoError(t, err) - assert.Equal(t, []string{ - "supabase/schemas/z_function.sql", - "supabase/schemas/tables/a_table.sql", - "supabase/schemas/tables/nested/b_table.sql", - }, files) - }) - - t.Run("deduplicates explicit files and directory matches", func(t *testing.T) { - fsys := fs.MapFS{ - "supabase/database/a.sql": &fs.MapFile{Data: []byte("select 1;")}, - "supabase/database/b.sql": &fs.MapFile{Data: []byte("select 2;")}, - } - g := Glob{ - "supabase/database/a.sql", - "supabase/database", - "supabase/database/*.sql", - } - - files, err := g.SQLFiles(fsys) - - assert.NoError(t, err) - assert.Equal(t, []string{ - "supabase/database/a.sql", - "supabase/database/b.sql", - }, files) - }) -} - -func TestLoadFunctionImportMap(t *testing.T) { - t.Run("uses deno.json as import map when present", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` - project_id = "bvikqvbczudanvggcord" - [functions.hello] - `)}, - "supabase/functions/hello/deno.json": &fs.MapFile{}, - "supabase/functions/hello/index.ts": &fs.MapFile{}, - } - // Run test - assert.NoError(t, config.Load("", fsys)) - // Check that deno.json was set as import map - assert.Equal(t, "supabase/functions/hello/deno.json", config.Functions["hello"].ImportMap) - assert.Nil(t, config.Functions["hello"].VerifyJWT) - }) - - t.Run("uses deno.jsonc as import map when present", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` - project_id = "bvikqvbczudanvggcord" - [functions.hello] - `)}, - "supabase/functions/hello/deno.jsonc": &fs.MapFile{}, - "supabase/functions/hello/index.ts": &fs.MapFile{}, - } - // Run test - assert.NoError(t, config.Load("", fsys)) - // Check that deno.jsonc was set as import map - assert.Equal(t, "supabase/functions/hello/deno.jsonc", config.Functions["hello"].ImportMap) - }) - - t.Run("config.toml takes precedence over deno.json", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` - project_id = "bvikqvbczudanvggcord" - [functions] - hello.import_map = "custom_import_map.json" - `)}, - "supabase/functions/hello/deno.json": &fs.MapFile{}, - "supabase/functions/hello/index.ts": &fs.MapFile{}, - } - // Run test - assert.NoError(t, config.Load("", fsys)) - // Check that config.toml takes precedence over deno.json - assert.Equal(t, "supabase/custom_import_map.json", config.Functions["hello"].ImportMap) - }) -} - -func TestLoadFunctionErrorMessageParsing(t *testing.T) { - t.Run("returns error for array-style function config", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` - project_id = "bvikqvbczudanvggcord" - [[functions]] - name = "hello" - verify_jwt = true - `)}, - } - // Run test - err := config.Load("", fsys) - // Check error contains both decode errors - assert.ErrorContains(t, err, invalidFunctionsConfigFormat) - }) - - t.Run("returns error with function slug for invalid non-existent field", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` - project_id = "bvikqvbczudanvggcord" - [functions.hello] - unknown_field = true - `)}, - } - // Run test - err := config.Load("", fsys) - // Check error contains both decode errors - assert.ErrorContains(t, err, "'functions[hello]' has invalid keys: unknown_field") - }) - - t.Run("returns error with function slug for invalid field value", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` - project_id = "bvikqvbczudanvggcord" - [functions.hello] - verify_jwt = "not-a-bool" - `)}, - } - // Run test - err := config.Load("", fsys) - // Check error contains both decode errors - assert.ErrorContains(t, err, `'functions[hello].verify_jwt' cannot parse value as 'bool': strconv.ParseBool: invalid syntax`) - }) - - t.Run("returns error for unknown function fields", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` - project_id = "bvikqvbczudanvggcord" - [functions] - name = "hello" - verify_jwt = true - `)}, - } - // Run test - err := config.Load("", fsys) - assert.ErrorContains(t, err, `'functions[name]' expected a map or struct, got "string"`) - assert.ErrorContains(t, err, `'functions[verify_jwt]' expected a map or struct, got "bool"`) - }) -} - -func TestLoadEnvIfExists(t *testing.T) { - t.Run("returns nil when file does not exist", func(t *testing.T) { - err := loadEnvIfExists("nonexistent.env") - assert.NoError(t, err) - }) - - t.Run("returns raw error when file exists but is malformed and DEBUG=1", func(t *testing.T) { - // Set DEBUG=1 - t.Setenv("DEBUG", "1") - viper.AutomaticEnv() - - // Create a temporary file with malformed content - tmpFile, err := os.CreateTemp("", "test-*.env") - require.NoError(t, err) - defer os.Remove(tmpFile.Name()) - - // Write malformed content - _, err = tmpFile.WriteString("[invalid]\nvalue=secret_value\n") - require.NoError(t, err) - require.NoError(t, tmpFile.Close()) - - // Test loading the malformed file - err = loadEnvIfExists(tmpFile.Name()) - // Should contain the raw error, including the secret value - assert.ErrorContains(t, err, "unexpected character") - assert.ErrorContains(t, err, "secret_value") - }) - - t.Run("returns error when file exists but is malformed invalid character", func(t *testing.T) { - // Create a temporary file with malformed content - tmpFile, err := os.CreateTemp("", "test-*.env") - require.NoError(t, err) - defer os.Remove(tmpFile.Name()) - - // Write malformed content - _, err = tmpFile.WriteString("[invalid]\nvalue=secret_value\n") - require.NoError(t, err) - require.NoError(t, tmpFile.Close()) - - // Test loading the malformed file - err = loadEnvIfExists(tmpFile.Name()) - assert.ErrorContains(t, err, fmt.Sprintf("failed to parse environment file: %s (unexpected character '[' in variable name)", tmpFile.Name())) - assert.NotContains(t, err.Error(), "secret_value") - }) - - t.Run("returns error when file exists but is malformed unterminated quotes", func(t *testing.T) { - // Create a temporary file with malformed content - tmpFile, err := os.CreateTemp("", "test-*.env") - require.NoError(t, err) - defer os.Remove(tmpFile.Name()) - - // Write malformed content - _, err = tmpFile.WriteString("value=\"secret_value\n") - require.NoError(t, err) - require.NoError(t, tmpFile.Close()) - - // Test loading the malformed file - err = loadEnvIfExists(tmpFile.Name()) - assert.ErrorContains(t, err, fmt.Sprintf("failed to parse environment file: %s (unterminated quoted value)", tmpFile.Name())) - assert.NotContains(t, err.Error(), "secret_value") - }) - - t.Run("loads valid env file successfully", func(t *testing.T) { - // Create a temporary file with valid content - tmpFile, err := os.CreateTemp("", "test-*.env") - require.NoError(t, err) - defer os.Remove(tmpFile.Name()) - - // Write valid content - _, err = tmpFile.WriteString("TEST_KEY=test_value\nANOTHER_KEY=another_value") - require.NoError(t, err) - require.NoError(t, tmpFile.Close()) - - // Test loading the valid file - err = loadEnvIfExists(tmpFile.Name()) - assert.NoError(t, err) - - // Verify environment variables were loaded - assert.Equal(t, "test_value", os.Getenv("TEST_KEY")) - assert.Equal(t, "another_value", os.Getenv("ANOTHER_KEY")) - - // Clean up environment variables - os.Unsetenv("TEST_KEY") - os.Unsetenv("ANOTHER_KEY") - }) -} - -func TestVersionCompare(t *testing.T) { - var testcase = []struct { - a string - b string - r int - }{ - {"15.1.0.55", "15.1.0.55", 0}, - {"15.8.1.085", "15.1.0.55", 1}, - {"15.1.0.55", "15.8.1.085", -1}, - {"17.4.1.005", "17.4.1.005", 0}, - {"17.4.1.030", "17.4.1.005", 1}, - {"17.4.1.005", "17.4.1.030", -1}, - {"15.8.1", "15.8.1", 0}, - {"17", "15.8", 1}, - {"14", "15.8", -1}, - {"oriole-17", "oriole-17", 0}, - {"17", "oriole-17", 1}, - {"oriole-17", "17", -1}, - } - - for _, tt := range testcase { - t.Run(fmt.Sprintf("%s vs %s", tt.a, tt.b), func(t *testing.T) { - result := VersionCompare(tt.a, tt.b) - assert.Equal(t, tt.r, result) - }) - } -} - -func TestDeprecatedSMTPConfig(t *testing.T) { - t.Run("maps deprecated [inbucket] to local_smtp", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` -[inbucket] -enabled = true -port = 12345 -`)}, - } - require.NoError(t, config.Load("", fsys)) - assert.True(t, config.Inbucket.Enabled) - assert.Equal(t, uint16(12345), config.Inbucket.Port) - }) - - t.Run("keeps template defaults for a partial [inbucket] section", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` -[inbucket] -port = 9999 -`)}, - } - require.NoError(t, config.Load("", fsys)) - // enabled is omitted by the user; the template default (true) must survive - // the inbucket -> local_smtp rewrite via deep merge instead of collapsing - // to the zero value. - assert.True(t, config.Inbucket.Enabled) - assert.Equal(t, uint16(9999), config.Inbucket.Port) - }) - - t.Run("prefers explicit [local_smtp] over deprecated [inbucket]", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` -[inbucket] -enabled = true -port = 11111 - -[local_smtp] -enabled = true -port = 22222 -`)}, - } - require.NoError(t, config.Load("", fsys)) - assert.Equal(t, uint16(22222), config.Inbucket.Port) - }) - - t.Run("normalizes deprecated [remotes.*.inbucket]", func(t *testing.T) { - config := NewConfig() - config.ProjectId = "abcdefghijklmnopqrst" - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` -[remotes.staging] -project_id = "abcdefghijklmnopqrst" - -[remotes.staging.inbucket] -enabled = true -port = 33333 -`)}, - } - require.NoError(t, config.Load("", fsys)) - assert.Equal(t, uint16(33333), config.Inbucket.Port) - }) - - t.Run("preserves env overrides when rewriting [inbucket]", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` -[inbucket] -enabled = true -port = 12345 -`)}, - } - // Env overrides are applied via ExperimentalBindStruct at unmarshal time, not - // captured by AllSettings(). Rebuilding the viper without those options while - // rewriting [inbucket] would silently drop this override. - t.Setenv("SUPABASE_AUTH_SITE_URL", "http://env-override.example/") - require.NoError(t, config.Load("", fsys)) - assert.Equal(t, "http://env-override.example/", config.Auth.SiteUrl) - assert.Equal(t, uint16(12345), config.Inbucket.Port) - }) -} - -func TestDeprecatedOrioleDBVersionConfig(t *testing.T) { - captureStderr := func(t *testing.T, run func()) string { - t.Helper() - r, w, err := os.Pipe() - require.NoError(t, err) - defer r.Close() - defer w.Close() - orig := os.Stderr - os.Stderr = w - defer func() { os.Stderr = orig }() - run() - require.NoError(t, w.Close()) - var out bytes.Buffer - _, err = io.Copy(&out, r) - require.NoError(t, err) - return out.String() - } - - t.Run("promotes deprecated [experimental] orioledb_version to [db]", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` -[experimental] -orioledb_version = "15.1.0.150" -`)}, - } - stderr := captureStderr(t, func() { - require.NoError(t, config.Load("", fsys)) - }) - assert.Equal(t, "15.1.0.150", config.Db.OrioleDBVersion) - assert.Contains(t, stderr, "WARN: experimental.orioledb_version is deprecated. Please use db.orioledb_version instead.") - }) - - t.Run("does not warn when [experimental] orioledb_version is empty", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` -[experimental] -orioledb_version = "" -`)}, - } - stderr := captureStderr(t, func() { - require.NoError(t, config.Load("", fsys)) - }) - assert.Equal(t, "", config.Db.OrioleDBVersion) - assert.NotContains(t, stderr, "orioledb_version is deprecated") - }) - - t.Run("promotes deprecated [experimental] orioledb_version when [db] is explicitly empty", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` -[experimental] -orioledb_version = "x" - -[db] -orioledb_version = "" -`)}, - } - stderr := captureStderr(t, func() { - require.NoError(t, config.Load("", fsys)) - }) - assert.Equal(t, "x", config.Db.OrioleDBVersion) - assert.Contains(t, stderr, "WARN: experimental.orioledb_version is deprecated. Please use db.orioledb_version instead.") - }) - - t.Run("prefers explicit [db] orioledb_version over deprecated [experimental]", func(t *testing.T) { - config := NewConfig() - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` -[experimental] -orioledb_version = "15.1.0.150" - -[db] -orioledb_version = "15.1.1.13" -`)}, - } - stderr := captureStderr(t, func() { - require.NoError(t, config.Load("", fsys)) - }) - assert.Equal(t, "15.1.1.13", config.Db.OrioleDBVersion) - assert.Contains(t, stderr, "WARN: experimental.orioledb_version is deprecated. Please use db.orioledb_version instead.") - }) - - t.Run("normalizes deprecated [remotes.*.experimental] orioledb_version", func(t *testing.T) { - config := NewConfig() - config.ProjectId = "abcdefghijklmnopqrst" - fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: []byte(` -[remotes.staging] -project_id = "abcdefghijklmnopqrst" - -[remotes.staging.experimental] -orioledb_version = "15.1.0.150" -`)}, - } - stderr := captureStderr(t, func() { - require.NoError(t, config.Load("", fsys)) - }) - assert.Equal(t, "15.1.0.150", config.Db.OrioleDBVersion) - assert.Contains(t, stderr, "WARN: remotes.staging.experimental.orioledb_version is deprecated. Please use remotes.staging.db.orioledb_version instead.") - }) -} - -func TestLoadLinkedStateFiles(t *testing.T) { - linkedFiles := fs.MapFS{ - "supabase/.temp/pooler-url": &fs.MapFile{Data: []byte("postgresql://postgres.decoy:[YOUR-PASSWORD]@pooler.example.com:6543/postgres")}, - "supabase/.temp/rest-version": &fs.MapFile{Data: []byte("v0.0.1-decoy")}, - "supabase/.temp/gotrue-version": &fs.MapFile{Data: []byte("v0.0.2-decoy")}, - "supabase/.temp/storage-version": &fs.MapFile{Data: []byte("v0.0.3-decoy")}, - "supabase/.temp/storage-migration": &fs.MapFile{Data: []byte("decoy-migration")}, - "supabase/.temp/edge-runtime-version": &fs.MapFile{Data: []byte("v0.0.4-decoy")}, - } - - t.Run("legacy mode applies the pooler url and image pins", func(t *testing.T) { - t.Setenv(StackStateEnv, "") - config := NewConfig() - require.NoError(t, config.Load("", linkedFiles)) - assert.Contains(t, config.Db.Pooler.ConnectionString, "pooler.example.com") - assert.True(t, strings.HasSuffix(config.Api.Image, ":v0.0.1-decoy"), config.Api.Image) - assert.True(t, strings.HasSuffix(config.Auth.Image, ":v0.0.2-decoy"), config.Auth.Image) - assert.True(t, strings.HasSuffix(config.Storage.Image, ":v0.0.3-decoy"), config.Storage.Image) - assert.Equal(t, "decoy-migration", config.Storage.TargetMigration) - assert.True(t, strings.HasSuffix(config.EdgeRuntime.Image, ":v0.0.4-decoy"), config.EdgeRuntime.Image) - }) - - t.Run("stack mode ignores the pooler url file and image pins", func(t *testing.T) { - t.Setenv(StackStateEnv, "1") - t.Setenv(StackPoolerUrlEnv, "") - want := NewConfig() - require.NoError(t, want.Load("", fs.MapFS{})) - config := NewConfig() - require.NoError(t, config.Load("", linkedFiles)) - assert.Equal(t, want.Db.Pooler.ConnectionString, config.Db.Pooler.ConnectionString) - assert.Equal(t, want.Api.Image, config.Api.Image) - assert.Equal(t, want.Auth.Image, config.Auth.Image) - assert.Equal(t, want.Storage.Image, config.Storage.Image) - assert.Equal(t, want.Storage.TargetMigration, config.Storage.TargetMigration) - assert.Equal(t, want.EdgeRuntime.Image, config.EdgeRuntime.Image) - }) - - t.Run("stack mode applies the pooler url from the environment", func(t *testing.T) { - const poolerUrl = "postgresql://postgres.abcdefghijklmnopqrst:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:6543/postgres" - t.Setenv(StackStateEnv, "1") - t.Setenv(StackPoolerUrlEnv, poolerUrl) - config := NewConfig() - require.NoError(t, config.Load("", linkedFiles)) - assert.Equal(t, poolerUrl, config.Db.Pooler.ConnectionString) - }) -} diff --git a/apps/cli-go/pkg/config/constants.go b/apps/cli-go/pkg/config/constants.go deleted file mode 100644 index 93bb482f69..0000000000 --- a/apps/cli-go/pkg/config/constants.go +++ /dev/null @@ -1,77 +0,0 @@ -package config - -import ( - _ "embed" - "regexp" - - "github.com/go-errors/errors" - "github.com/go-viper/mapstructure/v2" -) - -const ( - deno1 = "supabase/edge-runtime:v1.68.4" -) - -type images struct { - Pg string `mapstructure:"pg"` - // PG13/15 and PG14 come from the Dockerfile's generated `pg15`/hand-pinned `pg14` stages - // (the single version table), not hardcoded constants. - Pg15 string `mapstructure:"pg15"` - Pg14 string `mapstructure:"pg14"` - // Append to Services when adding new dependencies below - Kong string `mapstructure:"kong"` - Inbucket string `mapstructure:"mailpit"` - Postgrest string `mapstructure:"postgrest"` - Pgmeta string `mapstructure:"pgmeta"` - Studio string `mapstructure:"studio"` - ImgProxy string `mapstructure:"imgproxy"` - EdgeRuntime string `mapstructure:"edgeruntime"` - Vector string `mapstructure:"vector"` - Supavisor string `mapstructure:"supavisor"` - Gotrue string `mapstructure:"gotrue"` - Realtime string `mapstructure:"realtime"` - Storage string `mapstructure:"storage"` - Logflare string `mapstructure:"logflare"` - // Append to Jobs when adding new dependencies below - Differ string `mapstructure:"differ"` - Migra string `mapstructure:"migra"` - PgProve string `mapstructure:"pgprove"` -} - -var ( - //go:embed templates/Dockerfile - dockerImage string - imagePattern = regexp.MustCompile(`(?i)FROM\s+([^\s]+)\s+AS\s+([^\s]+)`) - Images images -) - -func init() { - matches := imagePattern.FindAllStringSubmatch(dockerImage, -1) - result := make(map[string]string, len(matches)) - for _, m := range matches { - if len(m) == 3 { - result[m[2]] = m[1] - } - } - if err := mapstructure.Decode(result, &Images); err != nil { - panic(errors.Errorf("failed to decode images: %w", err)) - } -} - -func (s images) Services() []string { - return []string{ - s.Gotrue, - s.Realtime, - s.Storage, - s.ImgProxy, - s.Kong, - s.Inbucket, - s.Postgrest, - s.Pgmeta, - s.Studio, - s.EdgeRuntime, - s.Logflare, - s.Vector, - s.Supavisor, - } -} diff --git a/apps/cli-go/pkg/config/constants_test.go b/apps/cli-go/pkg/config/constants_test.go deleted file mode 100644 index 1f8badef39..0000000000 --- a/apps/cli-go/pkg/config/constants_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package config - -import ( - "strings" - "testing" - - "github.com/go-viper/mapstructure/v2" - "github.com/stretchr/testify/assert" -) - -func TestParseImage(t *testing.T) { - output := map[string]string{} - err := mapstructure.Decode(Images, &output) - assert.NoError(t, err) - assert.Len(t, output, strings.Count(dockerImage, "FROM")) - for _, v := range output { - assert.NotEmpty(t, v) - } -} diff --git a/apps/cli-go/pkg/config/db.go b/apps/cli-go/pkg/config/db.go deleted file mode 100644 index 821c0a554b..0000000000 --- a/apps/cli-go/pkg/config/db.go +++ /dev/null @@ -1,269 +0,0 @@ -package config - -import ( - "bytes" - "slices" - "time" - - "github.com/go-errors/errors" - v1API "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/cast" - "github.com/supabase/cli/pkg/diff" -) - -type PoolMode string - -const ( - TransactionMode PoolMode = "transaction" - SessionMode PoolMode = "session" -) - -func (m *PoolMode) UnmarshalText(text []byte) error { - allowed := []PoolMode{TransactionMode, SessionMode} - if *m = PoolMode(text); !slices.Contains(allowed, *m) { - return errors.Errorf("must be one of %v", allowed) - } - return nil -} - -type SessionReplicationRole string - -const ( - SessionReplicationRoleOrigin SessionReplicationRole = "origin" - SessionReplicationRoleReplica SessionReplicationRole = "replica" - SessionReplicationRoleLocal SessionReplicationRole = "local" -) - -func (r *SessionReplicationRole) UnmarshalText(text []byte) error { - allowed := []SessionReplicationRole{SessionReplicationRoleOrigin, SessionReplicationRoleReplica, SessionReplicationRoleLocal} - if *r = SessionReplicationRole(text); !slices.Contains(allowed, *r) { - return errors.Errorf("must be one of %v", allowed) - } - return nil -} - -type ( - settings struct { - EffectiveCacheSize *string `toml:"effective_cache_size" json:"effective_cache_size"` - LogicalDecodingWorkMem *string `toml:"logical_decoding_work_mem" json:"logical_decoding_work_mem"` - MaintenanceWorkMem *string `toml:"maintenance_work_mem" json:"maintenance_work_mem"` - MaxConnections *uint `toml:"max_connections" json:"max_connections"` - MaxLocksPerTransaction *uint `toml:"max_locks_per_transaction" json:"max_locks_per_transaction"` - MaxParallelMaintenanceWorkers *uint `toml:"max_parallel_maintenance_workers" json:"max_parallel_maintenance_workers"` - MaxParallelWorkers *uint `toml:"max_parallel_workers" json:"max_parallel_workers"` - MaxParallelWorkersPerGather *uint `toml:"max_parallel_workers_per_gather" json:"max_parallel_workers_per_gather"` - MaxReplicationSlots *uint `toml:"max_replication_slots" json:"max_replication_slots"` - MaxSlotWalKeepSize *string `toml:"max_slot_wal_keep_size" json:"max_slot_wal_keep_size"` - MaxStandbyArchiveDelay *string `toml:"max_standby_archive_delay" json:"max_standby_archive_delay"` - MaxStandbyStreamingDelay *string `toml:"max_standby_streaming_delay" json:"max_standby_streaming_delay"` - MaxWalSize *string `toml:"max_wal_size" json:"max_wal_size"` - MaxWalSenders *uint `toml:"max_wal_senders" json:"max_wal_senders"` - MaxWorkerProcesses *uint `toml:"max_worker_processes" json:"max_worker_processes"` - SessionReplicationRole *SessionReplicationRole `toml:"session_replication_role" json:"session_replication_role"` - SharedBuffers *string `toml:"shared_buffers" json:"shared_buffers"` - StatementTimeout *string `toml:"statement_timeout" json:"statement_timeout"` - TrackActivityQuerySize *string `toml:"track_activity_query_size" json:"track_activity_query_size"` - TrackCommitTimestamp *bool `toml:"track_commit_timestamp" json:"track_commit_timestamp"` - WalKeepSize *string `toml:"wal_keep_size" json:"wal_keep_size"` - WalSenderTimeout *string `toml:"wal_sender_timeout" json:"wal_sender_timeout"` - WorkMem *string `toml:"work_mem" json:"work_mem"` - } - - networkRestrictions struct { - Enabled bool `toml:"enabled" json:"enabled"` - AllowedCidrs []string `toml:"allowed_cidrs" json:"allowed_cidrs"` - AllowedCidrsV6 []string `toml:"allowed_cidrs_v6" json:"allowed_cidrs_v6"` - } - - sslEnforcement struct { - Enabled bool `toml:"enabled" json:"enabled"` - } - - db struct { - Image string `toml:"-" json:"-"` - Port uint16 `toml:"port" json:"port"` - ShadowPort uint16 `toml:"shadow_port" json:"shadow_port"` - HealthTimeout time.Duration `toml:"health_timeout" json:"health_timeout"` - MajorVersion uint `toml:"major_version" json:"major_version"` - OrioleDBVersion string `toml:"orioledb_version" json:"orioledb_version"` - Password string `toml:"-" json:"-"` - RootKey Secret `toml:"root_key" json:"root_key"` - Pooler pooler `toml:"pooler" json:"pooler"` - Migrations migrations `toml:"migrations" json:"migrations"` - Seed seed `toml:"seed" json:"seed"` - Settings settings `toml:"settings" json:"settings"` - NetworkRestrictions networkRestrictions `toml:"network_restrictions" json:"network_restrictions"` - SslEnforcement *sslEnforcement `toml:"ssl_enforcement" json:"ssl_enforcement"` - Vault map[string]Secret `toml:"vault" json:"vault"` - } - - migrations struct { - Enabled bool `toml:"enabled" json:"enabled"` - SchemaPaths Glob `toml:"schema_paths" json:"schema_paths"` - } - - seed struct { - Enabled bool `toml:"enabled" json:"enabled"` - SqlPaths Glob `toml:"sql_paths" json:"sql_paths"` - } - - pooler struct { - Enabled bool `toml:"enabled" json:"enabled"` - Image string `toml:"-" json:"-"` - Port uint16 `toml:"port" json:"port"` - PoolMode PoolMode `toml:"pool_mode" json:"pool_mode"` - DefaultPoolSize uint `toml:"default_pool_size" json:"default_pool_size"` - MaxClientConn uint `toml:"max_client_conn" json:"max_client_conn"` - ConnectionString string `toml:"-" json:"-"` - TenantId string `toml:"-" json:"-"` - EncryptionKey string `toml:"-" json:"-"` - SecretKeyBase string `toml:"-" json:"-"` - } -) - -func (a *settings) ToUpdatePostgresConfigBody() v1API.UpdatePostgresConfigBody { - body := v1API.UpdatePostgresConfigBody{} - - // Parameters that require restart - body.MaxConnections = cast.UintToIntPtr(a.MaxConnections) - body.MaxWorkerProcesses = cast.UintToIntPtr(a.MaxWorkerProcesses) - body.MaxParallelWorkers = cast.UintToIntPtr(a.MaxParallelWorkers) - body.MaxWalSenders = cast.UintToIntPtr(a.MaxWalSenders) - body.MaxReplicationSlots = cast.UintToIntPtr(a.MaxReplicationSlots) - body.SharedBuffers = a.SharedBuffers - - // Parameters that can be changed without restart - body.EffectiveCacheSize = a.EffectiveCacheSize - body.LogicalDecodingWorkMem = a.LogicalDecodingWorkMem - body.MaintenanceWorkMem = a.MaintenanceWorkMem - body.MaxLocksPerTransaction = cast.UintToIntPtr(a.MaxLocksPerTransaction) - body.MaxParallelMaintenanceWorkers = cast.UintToIntPtr(a.MaxParallelMaintenanceWorkers) - body.MaxParallelWorkersPerGather = cast.UintToIntPtr(a.MaxParallelWorkersPerGather) - body.MaxSlotWalKeepSize = a.MaxSlotWalKeepSize - body.MaxStandbyArchiveDelay = a.MaxStandbyArchiveDelay - body.MaxStandbyStreamingDelay = a.MaxStandbyStreamingDelay - body.MaxWalSize = a.MaxWalSize - body.SessionReplicationRole = (*v1API.UpdatePostgresConfigBodySessionReplicationRole)(a.SessionReplicationRole) - body.StatementTimeout = a.StatementTimeout - body.TrackActivityQuerySize = a.TrackActivityQuerySize - body.TrackCommitTimestamp = a.TrackCommitTimestamp - body.WalKeepSize = a.WalKeepSize - body.WalSenderTimeout = a.WalSenderTimeout - body.WorkMem = a.WorkMem - return body -} - -func (a *settings) FromRemotePostgresConfig(remoteConfig v1API.PostgresConfigResponseOutput) { - a.EffectiveCacheSize = remoteConfig.EffectiveCacheSize - a.LogicalDecodingWorkMem = remoteConfig.LogicalDecodingWorkMem - a.MaintenanceWorkMem = remoteConfig.MaintenanceWorkMem - a.MaxConnections = cast.IntToUintPtr(remoteConfig.MaxConnections) - a.MaxLocksPerTransaction = cast.IntToUintPtr(remoteConfig.MaxLocksPerTransaction) - a.MaxParallelMaintenanceWorkers = cast.IntToUintPtr(remoteConfig.MaxParallelMaintenanceWorkers) - a.MaxParallelWorkers = cast.IntToUintPtr(remoteConfig.MaxParallelWorkers) - a.MaxParallelWorkersPerGather = cast.IntToUintPtr(remoteConfig.MaxParallelWorkersPerGather) - a.MaxReplicationSlots = cast.IntToUintPtr(remoteConfig.MaxReplicationSlots) - a.MaxSlotWalKeepSize = remoteConfig.MaxSlotWalKeepSize - a.MaxStandbyArchiveDelay = remoteConfig.MaxStandbyArchiveDelay - a.MaxStandbyStreamingDelay = remoteConfig.MaxStandbyStreamingDelay - a.MaxWalSenders = cast.IntToUintPtr(remoteConfig.MaxWalSenders) - a.MaxWalSize = remoteConfig.MaxWalSize - a.MaxWorkerProcesses = cast.IntToUintPtr(remoteConfig.MaxWorkerProcesses) - a.SessionReplicationRole = (*SessionReplicationRole)(remoteConfig.SessionReplicationRole) - a.SharedBuffers = remoteConfig.SharedBuffers - a.StatementTimeout = remoteConfig.StatementTimeout - a.TrackActivityQuerySize = remoteConfig.TrackActivityQuerySize - a.TrackCommitTimestamp = remoteConfig.TrackCommitTimestamp - a.WalKeepSize = remoteConfig.WalKeepSize - a.WalSenderTimeout = remoteConfig.WalSenderTimeout - a.WorkMem = remoteConfig.WorkMem -} - -const pgConfHeader = "\n# supabase [db.settings] configuration\n" - -// create a valid string to append to /etc/postgresql/postgresql.conf -func (a *settings) ToPostgresConfig() string { - // Assuming postgres settings is always a flat struct, we can serialise - // using toml, then replace double quotes with single. - data, _ := ToTomlBytes(*a) - body := bytes.ReplaceAll(data, []byte{'"'}, []byte{'\''}) - return pgConfHeader + string(body) -} - -func (a *settings) DiffWithRemote(remoteConfig v1API.PostgresConfigResponseOutput) ([]byte, error) { - copy := *a - // Convert the config values into easily comparable remoteConfig values - currentValue, err := ToTomlBytes(copy) - if err != nil { - return nil, err - } - copy.FromRemotePostgresConfig(remoteConfig) - remoteCompare, err := ToTomlBytes(copy) - if err != nil { - return nil, err - } - return diff.Diff("remote[db.settings]", remoteCompare, "local[db.settings]", currentValue), nil -} - -func (n networkRestrictions) ToUpdateNetworkRestrictionsBody() v1API.V1UpdateNetworkRestrictionsJSONRequestBody { - body := v1API.V1UpdateNetworkRestrictionsJSONRequestBody{ - DbAllowedCidrs: &n.AllowedCidrs, - DbAllowedCidrsV6: &n.AllowedCidrsV6, - } - return body -} - -func (n *networkRestrictions) FromRemoteNetworkRestrictions(remoteConfig v1API.NetworkRestrictionsResponseOutput) { - if !n.Enabled { - return - } - if remoteConfig.Config.DbAllowedCidrs != nil { - n.AllowedCidrs = *remoteConfig.Config.DbAllowedCidrs - } - if remoteConfig.Config.DbAllowedCidrsV6 != nil { - n.AllowedCidrsV6 = *remoteConfig.Config.DbAllowedCidrsV6 - } -} - -func (n *networkRestrictions) DiffWithRemote(remoteConfig v1API.NetworkRestrictionsResponseOutput) ([]byte, error) { - copy := *n - // Convert the config values into easily comparable remoteConfig values - currentValue, err := ToTomlBytes(copy) - if err != nil { - return nil, err - } - copy.FromRemoteNetworkRestrictions(remoteConfig) - remoteCompare, err := ToTomlBytes(copy) - if err != nil { - return nil, err - } - return diff.Diff("remote[db.network_restrictions]", remoteCompare, "local[db.network_restrictions]", currentValue), nil -} - -func (s sslEnforcement) ToUpdateSslEnforcementBody() v1API.V1UpdateSslEnforcementConfigJSONRequestBody { - body := v1API.V1UpdateSslEnforcementConfigJSONRequestBody{} - body.RequestedConfig.Database = s.Enabled - return body -} - -func (s *sslEnforcement) FromRemoteSslEnforcement(remoteConfig v1API.SslEnforcementResponseOutput) { - if s == nil { - return - } - s.Enabled = remoteConfig.CurrentConfig.Database -} - -func (s *sslEnforcement) DiffWithRemote(remoteConfig v1API.SslEnforcementResponseOutput) ([]byte, error) { - copy := *s - // Convert the config values into easily comparable remoteConfig values - currentValue, err := ToTomlBytes(copy) - if err != nil { - return nil, err - } - copy.FromRemoteSslEnforcement(remoteConfig) - remoteCompare, err := ToTomlBytes(copy) - if err != nil { - return nil, err - } - return diff.Diff("remote[db.ssl_enforcement]", remoteCompare, "local[db.ssl_enforcement]", currentValue), nil -} diff --git a/apps/cli-go/pkg/config/db_test.go b/apps/cli-go/pkg/config/db_test.go deleted file mode 100644 index 2e23ec9563..0000000000 --- a/apps/cli-go/pkg/config/db_test.go +++ /dev/null @@ -1,281 +0,0 @@ -package config - -import ( - "testing" - - "github.com/stretchr/testify/assert" - v1API "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/cast" -) - -func TestDbSettingsToUpdatePostgresConfigBody(t *testing.T) { - t.Run("converts all fields correctly", func(t *testing.T) { - db := &db{ - Settings: settings{ - EffectiveCacheSize: cast.Ptr("4GB"), - MaxConnections: cast.Ptr(uint(100)), - SharedBuffers: cast.Ptr("1GB"), - StatementTimeout: cast.Ptr("30s"), - SessionReplicationRole: cast.Ptr(SessionReplicationRoleReplica), - }, - } - - body := db.Settings.ToUpdatePostgresConfigBody() - - assert.Equal(t, "4GB", *body.EffectiveCacheSize) - assert.Equal(t, 100, *body.MaxConnections) - assert.Equal(t, "1GB", *body.SharedBuffers) - assert.Equal(t, "30s", *body.StatementTimeout) - assert.Equal(t, v1API.UpdatePostgresConfigBodySessionReplicationRoleReplica, *body.SessionReplicationRole) - }) - - t.Run("handles empty fields", func(t *testing.T) { - db := &db{} - - body := db.Settings.ToUpdatePostgresConfigBody() - - assert.Nil(t, body.EffectiveCacheSize) - assert.Nil(t, body.MaxConnections) - assert.Nil(t, body.SharedBuffers) - assert.Nil(t, body.StatementTimeout) - assert.Nil(t, body.SessionReplicationRole) - }) -} - -func TestDbSettingsDiff(t *testing.T) { - t.Run("detects differences", func(t *testing.T) { - db := &db{ - Settings: settings{ - EffectiveCacheSize: cast.Ptr("4GB"), - MaxConnections: cast.Ptr(uint(100)), - SharedBuffers: cast.Ptr("1GB"), - }, - } - - remoteConfig := v1API.PostgresConfigResponseOutput{ - EffectiveCacheSize: cast.Ptr("8GB"), - MaxConnections: cast.Ptr(200), - SharedBuffers: cast.Ptr("2GB"), - } - - diff, err := db.Settings.DiffWithRemote(remoteConfig) - assert.NoError(t, err) - - assertSnapshotEqual(t, diff) - }) - - t.Run("handles no differences", func(t *testing.T) { - db := &db{ - Settings: settings{ - EffectiveCacheSize: cast.Ptr("4GB"), - MaxConnections: cast.Ptr(uint(100)), - SharedBuffers: cast.Ptr("1GB"), - }, - } - - remoteConfig := v1API.PostgresConfigResponseOutput{ - EffectiveCacheSize: cast.Ptr("4GB"), - MaxConnections: cast.Ptr(100), - SharedBuffers: cast.Ptr("1GB"), - } - - diff, err := db.Settings.DiffWithRemote(remoteConfig) - assert.NoError(t, err) - - assert.Empty(t, diff) - }) - - t.Run("handles multiple schemas and search paths with spaces", func(t *testing.T) { - db := &db{ - Settings: settings{ - EffectiveCacheSize: cast.Ptr("4GB"), - MaxConnections: cast.Ptr(uint(100)), - SharedBuffers: cast.Ptr("1GB"), - }, - } - - remoteConfig := v1API.PostgresConfigResponseOutput{ - EffectiveCacheSize: cast.Ptr("4GB"), - MaxConnections: cast.Ptr(100), - SharedBuffers: cast.Ptr("1GB"), - } - - diff, err := db.Settings.DiffWithRemote(remoteConfig) - assert.NoError(t, err) - - assert.Empty(t, diff) - }) - - t.Run("handles api disabled on remote side", func(t *testing.T) { - db := &db{ - Settings: settings{ - EffectiveCacheSize: cast.Ptr("4GB"), - MaxConnections: cast.Ptr(uint(100)), - SharedBuffers: cast.Ptr("1GB"), - }, - } - - remoteConfig := v1API.PostgresConfigResponseOutput{ - // All fields are nil to simulate disabled API - } - - diff, err := db.Settings.DiffWithRemote(remoteConfig) - assert.NoError(t, err) - - assertSnapshotEqual(t, diff) - }) - - t.Run("handles api disabled on local side", func(t *testing.T) { - db := &db{ - Settings: settings{ - // All fields are nil to simulate disabled API - }, - } - - remoteConfig := v1API.PostgresConfigResponseOutput{ - EffectiveCacheSize: cast.Ptr("4GB"), - MaxConnections: cast.Ptr(100), - SharedBuffers: cast.Ptr("1GB"), - } - - diff, err := db.Settings.DiffWithRemote(remoteConfig) - assert.NoError(t, err) - - assertSnapshotEqual(t, diff) - }) -} - -func TestSettingsToPostgresConfig(t *testing.T) { - t.Run("Only set values should appear", func(t *testing.T) { - settings := settings{ - MaxConnections: cast.Ptr(uint(100)), - MaxLocksPerTransaction: cast.Ptr(uint(64)), - SharedBuffers: cast.Ptr("128MB"), - WorkMem: cast.Ptr("4MB"), - } - got := settings.ToPostgresConfig() - - assert.Contains(t, got, "max_connections = 100") - assert.Contains(t, got, "max_locks_per_transaction = 64") - assert.Contains(t, got, "shared_buffers = '128MB'") - assert.Contains(t, got, "work_mem = '4MB'") - - assert.NotContains(t, got, "effective_cache_size") - assert.NotContains(t, got, "maintenance_work_mem") - assert.NotContains(t, got, "max_parallel_workers") - }) - - t.Run("SessionReplicationRole should be handled correctly", func(t *testing.T) { - settings := settings{ - SessionReplicationRole: cast.Ptr(SessionReplicationRoleOrigin), - } - got := settings.ToPostgresConfig() - - assert.Contains(t, got, "session_replication_role = 'origin'") - }) - - t.Run("Empty settings should result in empty string", func(t *testing.T) { - settings := settings{} - got := settings.ToPostgresConfig() - - assert.Equal(t, got, "\n# supabase [db.settings] configuration\n") - assert.NotContains(t, got, "=") - }) -} - -func TestNetworkRestrictionsFromRemote(t *testing.T) { - t.Run("converts from remote config with restrictions", func(t *testing.T) { - ipv4Cidrs := []string{"192.168.1.0/24"} - ipv6Cidrs := []string{"2001:db8::/32"} - remoteConfig := v1API.NetworkRestrictionsResponseOutput{} - remoteConfig.Config.DbAllowedCidrs = &ipv4Cidrs - remoteConfig.Config.DbAllowedCidrsV6 = &ipv6Cidrs - nr := networkRestrictions{Enabled: true} - nr.FromRemoteNetworkRestrictions(remoteConfig) - assert.ElementsMatch(t, ipv4Cidrs, nr.AllowedCidrs) - assert.ElementsMatch(t, ipv6Cidrs, nr.AllowedCidrsV6) - }) - - t.Run("converts from remote config with allow all", func(t *testing.T) { - ipv4Cidrs := []string{"0.0.0.0/0"} - ipv6Cidrs := []string{"::/0"} - remoteConfig := v1API.NetworkRestrictionsResponseOutput{} - remoteConfig.Config.DbAllowedCidrs = &ipv4Cidrs - remoteConfig.Config.DbAllowedCidrsV6 = &ipv6Cidrs - nr := networkRestrictions{Enabled: true} - nr.FromRemoteNetworkRestrictions(remoteConfig) - assert.ElementsMatch(t, ipv4Cidrs, nr.AllowedCidrs) - assert.ElementsMatch(t, ipv6Cidrs, nr.AllowedCidrsV6) - }) - - t.Run("ignores locally disabled network restrictions", func(t *testing.T) { - remoteConfig := v1API.NetworkRestrictionsResponseOutput{} - remoteConfig.Config.DbAllowedCidrs = &[]string{"192.168.1.0/24"} - remoteConfig.Config.DbAllowedCidrsV6 = &[]string{"2001:db8::/32"} - nr := networkRestrictions{} - nr.FromRemoteNetworkRestrictions(remoteConfig) - assert.False(t, nr.Enabled) - assert.Empty(t, nr.AllowedCidrs) - assert.Empty(t, nr.AllowedCidrsV6) - }) -} - -func TestNetworkRestrictionsDiff(t *testing.T) { - t.Run("detects differences", func(t *testing.T) { - local := networkRestrictions{ - Enabled: true, - AllowedCidrs: []string{"192.168.1.0/24"}, - AllowedCidrsV6: []string{"2001:db8::/32"}, - } - remoteConfig := v1API.NetworkRestrictionsResponseOutput{} - remoteConfig.Config.DbAllowedCidrs = &[]string{"10.0.0.0/8"} - remoteConfig.Config.DbAllowedCidrsV6 = &[]string{"fd00::/8"} - diff, err := local.DiffWithRemote(remoteConfig) - assert.NoError(t, err) - assert.Contains(t, string(diff), "-allowed_cidrs = [\"10.0.0.0/8\"]") - assert.Contains(t, string(diff), "+allowed_cidrs = [\"192.168.1.0/24\"]") - assert.Contains(t, string(diff), "-allowed_cidrs_v6 = [\"fd00::/8\"]") - assert.Contains(t, string(diff), "+allowed_cidrs_v6 = [\"2001:db8::/32\"]") - }) - - t.Run("no differences", func(t *testing.T) { - local := networkRestrictions{ - Enabled: true, - AllowedCidrs: []string{"192.168.1.0/24"}, - AllowedCidrsV6: []string{"2001:db8::/32"}, - } - remoteConfig := v1API.NetworkRestrictionsResponseOutput{} - remoteConfig.Config.DbAllowedCidrs = &local.AllowedCidrs - remoteConfig.Config.DbAllowedCidrsV6 = &local.AllowedCidrsV6 - diff, err := local.DiffWithRemote(remoteConfig) - assert.NoError(t, err) - assert.Empty(t, diff) - }) - - t.Run("both have no restrictions - disabled vs allow all", func(t *testing.T) { - local := networkRestrictions{} - remoteConfig := v1API.NetworkRestrictionsResponseOutput{} - remoteConfig.Config.DbAllowedCidrs = &[]string{"0.0.0.0/0"} - remoteConfig.Config.DbAllowedCidrsV6 = &[]string{"::/0"} - diff, err := local.DiffWithRemote(remoteConfig) - assert.NoError(t, err) - assert.Empty(t, diff) - }) - - t.Run("local disallow all, remote allow all", func(t *testing.T) { - local := networkRestrictions{ - Enabled: true, - AllowedCidrs: []string{}, - AllowedCidrsV6: []string{}, - } - remoteConfig := v1API.NetworkRestrictionsResponseOutput{} - remoteConfig.Config.DbAllowedCidrs = &[]string{"0.0.0.0/0"} - remoteConfig.Config.DbAllowedCidrsV6 = &[]string{"::/0"} - diff, err := local.DiffWithRemote(remoteConfig) - assert.NoError(t, err) - assert.Contains(t, string(diff), "-allowed_cidrs = [\"0.0.0.0/0\"]") - assert.Contains(t, string(diff), "+allowed_cidrs = []") - assert.Contains(t, string(diff), "-allowed_cidrs_v6 = [\"::/0\"]") - assert.Contains(t, string(diff), "+allowed_cidrs_v6 = []") - }) -} diff --git a/apps/cli-go/pkg/config/decode_hooks.go b/apps/cli-go/pkg/config/decode_hooks.go deleted file mode 100644 index 265ec872e9..0000000000 --- a/apps/cli-go/pkg/config/decode_hooks.go +++ /dev/null @@ -1,50 +0,0 @@ -package config - -import ( - "os" - "reflect" - "regexp" - - "github.com/go-errors/errors" -) - -var envPattern = regexp.MustCompile(`^env\((.*)\)$`) - -// LoadEnvHook is a mapstructure decode hook that loads environment variables -// from strings formatted as env(VAR_NAME). -func LoadEnvHook(f reflect.Kind, t reflect.Kind, data any) (any, error) { - if f != reflect.String { - return data, nil - } - value := data.(string) - if matches := envPattern.FindStringSubmatch(value); len(matches) > 1 { - if env := os.Getenv(matches[1]); len(env) > 0 { - value = env - } - } - return value, nil -} - -const invalidFunctionsConfigFormat = `Invalid functions config format. Functions should be configured as: - -[functions.] -field = value - -Example: -[functions.hello] -verify_jwt = true` - -// ValidateFunctionsHook is a mapstructure decode hook that validates the functions config format. -func ValidateFunctionsHook(f reflect.Type, t reflect.Type, data any) (any, error) { - // Only handle FunctionConfig type - if t != reflect.TypeOf(FunctionConfig{}) { - return data, nil - } - - // Check if source is not a map - if f.Kind() != reflect.Map { - return nil, errors.New(invalidFunctionsConfigFormat) - } - - return data, nil -} diff --git a/apps/cli-go/pkg/config/secret.go b/apps/cli-go/pkg/config/secret.go deleted file mode 100644 index 8fa7f811b9..0000000000 --- a/apps/cli-go/pkg/config/secret.go +++ /dev/null @@ -1,109 +0,0 @@ -package config - -import ( - "encoding/base64" - "os" - "reflect" - "strings" - - ecies "github.com/ecies/go/v2" - "github.com/go-errors/errors" - "github.com/go-viper/mapstructure/v2" -) - -type Secret struct { - Value string - SHA256 string -} - -const HASHED_PREFIX = "hash:" - -func (s Secret) MarshalText() (text []byte, err error) { - if len(s.SHA256) == 0 { - return []byte{}, nil - } - return []byte(HASHED_PREFIX + s.SHA256), nil -} - -const ENCRYPTED_PREFIX = "encrypted:" - -func (s *Secret) Decrypt(keys []string) error { - if !strings.HasPrefix(s.Value, ENCRYPTED_PREFIX) { - return nil - } - if len(keys) == 0 { - return errors.New(errMissingKey) - } - var err error - for _, k := range keys { - // Use the first private key that successfully decrypts the secret - if s.Value, err = decrypt(k, s.Value); err == nil { - break - } - } - // If we didn't break, none of the keys worked - return err -} - -var errMissingKey = errors.New("missing private key") - -// Decrypt secret values following dotenvx convention: -// https://github.com/dotenvx/dotenvx/blob/main/src/lib/helpers/decryptKeyValue.js -func decrypt(key, value string) (string, error) { - if len(key) == 0 { - return value, errors.New(errMissingKey) - } - // Verify private key exists - privateKey, err := ecies.NewPrivateKeyFromHex(key) - if err != nil { - return value, errors.Errorf("failed to hex decode private key: %w", err) - } - // Verify ciphertext is base64 encoded - encoded := value[len(ENCRYPTED_PREFIX):] - ciphertext, err := base64.StdEncoding.DecodeString(encoded) - if err != nil { - return value, errors.Errorf("failed to base64 decode secret: %w", err) - } - // Return decrypted value - plaintext, err := ecies.Decrypt(privateKey, ciphertext) - if err != nil { - return value, errors.Errorf("failed to decrypt secret: %w", err) - } - return string(plaintext), nil -} - -const PRIVATE_KEY_PREFIX = "DOTENV_PRIVATE_KEY" - -func DecryptSecretHookFunc(hashKey string) mapstructure.DecodeHookFunc { - // Get all env vars and filter for private keys - var privateKeys []string - for _, env := range os.Environ() { - kv := strings.SplitN(env, "=", 2) - if kv[0] == PRIVATE_KEY_PREFIX || strings.HasPrefix(kv[0], PRIVATE_KEY_PREFIX+"_") { - privateKeys = append(privateKeys, strToArr(kv[1])...) - } - } - return func(f reflect.Type, t reflect.Type, data any) (any, error) { - if f.Kind() != reflect.String { - return data, nil - } - var result Secret - if t != reflect.TypeOf(result) { - return data, nil - } - result.Value = data.(string) - if len(result.Value) == 0 { - return result, nil - } - // Unloaded env() references should be returned verbatim without hashing - if envPattern.MatchString(result.Value) { - return result, nil - } - if err := result.Decrypt(privateKeys); err != nil { - return result, err - } - // Decrypted values should be hashed - result.SHA256 = sha256Hmac(hashKey, result.Value) - return result, nil - } -} diff --git a/apps/cli-go/pkg/config/secret_test.go b/apps/cli-go/pkg/config/secret_test.go deleted file mode 100644 index a83d07666b..0000000000 --- a/apps/cli-go/pkg/config/secret_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package config - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestDecryptSecret(t *testing.T) { - key := "7fd7210cef8f331ee8c55897996aaaafd853a2b20a4dc73d6d75759f65d2a7eb" - value := "encrypted:BKiXH15AyRzeohGyUrmB6cGjSklCrrBjdesQlX1VcXo/Xp20Bi2gGZ3AlIqxPQDmjVAALnhZamKnuY73l8Dz1P+BYiZUgxTSLzdCvdYUyVbNekj2UudbdUizBViERtZkuQwZHIv/" - - t.Run("decrypts secret value", func(t *testing.T) { - // Run test - plaintext, err := decrypt(key, value) - // Check error - assert.NoError(t, err) - assert.Equal(t, "value", plaintext) - }) - - t.Run("throws error on missing key", func(t *testing.T) { - // Run test - plaintext, err := decrypt("", value) - // Check error - assert.ErrorIs(t, err, errMissingKey) - assert.Equal(t, value, plaintext) - }) - - t.Run("throws error on non-hex key", func(t *testing.T) { - // Run test - plaintext, err := decrypt("invalid", value) - // Check error - assert.ErrorContains(t, err, "failed to hex decode private key: cannot decode hex string") - assert.Equal(t, value, plaintext) - }) - - t.Run("throws error on non-base64 value", func(t *testing.T) { - // Run test - plaintext, err := decrypt(key, "encrypted:invalid") - // Check error - assert.ErrorContains(t, err, "failed to base64 decode secret: illegal base64 data at input byte 4") - assert.Equal(t, "encrypted:invalid", plaintext) - }) - - t.Run("throws error on empty ciphertext", func(t *testing.T) { - // Run test - plaintext, err := decrypt(key, "encrypted:") - // Check error - assert.ErrorContains(t, err, "failed to decrypt secret: invalid length of message") - assert.Equal(t, "encrypted:", plaintext) - }) -} diff --git a/apps/cli-go/pkg/config/storage.go b/apps/cli-go/pkg/config/storage.go deleted file mode 100644 index a5d39ef080..0000000000 --- a/apps/cli-go/pkg/config/storage.go +++ /dev/null @@ -1,170 +0,0 @@ -package config - -import ( - v1API "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/cast" - "github.com/supabase/cli/pkg/diff" -) - -type ( - storage struct { - Enabled bool `toml:"enabled" json:"enabled"` - Image string `toml:"-" json:"-"` - TargetMigration string `toml:"-" json:"-"` - ImgProxyImage string `toml:"-" json:"-"` - FileSizeLimit sizeInBytes `toml:"file_size_limit" json:"file_size_limit"` - ImageTransformation *imageTransformation `toml:"image_transformation" json:"image_transformation"` - S3Protocol *s3Protocol `toml:"s3_protocol" json:"s3_protocol"` - S3Credentials storageS3Credentials `toml:"-" json:"-"` - Buckets BucketConfig `toml:"buckets" json:"buckets"` - AnalyticsBuckets analyticsBuckets `toml:"analytics" json:"analytics"` - VectorBuckets vectorBuckets `toml:"vector" json:"vector"` - } - - imageTransformation struct { - Enabled bool `toml:"enabled" json:"enabled"` - } - - analyticsBuckets struct { - Enabled bool `toml:"enabled" json:"enabled"` - MaxNamespaces uint `toml:"max_namespaces" json:"max_namespaces"` - MaxTables uint `toml:"max_tables" json:"max_tables"` - MaxCatalogs uint `toml:"max_catalogs" json:"max_catalogs"` - Buckets map[string]struct{} `toml:"buckets" json:"buckets"` - } - - vectorBuckets struct { - Enabled bool `toml:"enabled" json:"enabled"` - MaxBuckets uint `toml:"max_buckets" json:"max_buckets"` - MaxIndexes uint `toml:"max_indexes" json:"max_indexes"` - Buckets map[string]struct{} `toml:"buckets" json:"buckets"` - } - - s3Protocol struct { - Enabled bool `toml:"enabled" json:"enabled"` - } - - storageS3Credentials struct { - AccessKeyId string `toml:"-" json:"-"` - SecretAccessKey string `toml:"-" json:"-"` - Region string `toml:"-" json:"-"` - } - - BucketConfig map[string]bucket - - bucket struct { - Public *bool `toml:"public" json:"public"` - FileSizeLimit sizeInBytes `toml:"file_size_limit" json:"file_size_limit"` - AllowedMimeTypes []string `toml:"allowed_mime_types" json:"allowed_mime_types"` - ObjectsPath string `toml:"objects_path" json:"objects_path"` - } -) - -func (s *storage) ToUpdateStorageConfigBody() v1API.UpdateStorageConfigBody { - body := v1API.UpdateStorageConfigBody{ - FileSizeLimit: cast.Ptr(int64(s.FileSizeLimit)), - Features: &struct { - IcebergCatalog *struct { - Enabled bool `json:"enabled"` - MaxCatalogs int `json:"maxCatalogs"` - MaxNamespaces int `json:"maxNamespaces"` - MaxTables int `json:"maxTables"` - } `json:"icebergCatalog,omitempty"` - ImageTransformation *struct { - Enabled bool `json:"enabled"` - } `json:"imageTransformation,omitempty"` - PurgeCache *struct { - Enabled bool `json:"enabled"` - } `json:"purgeCache,omitempty"` - S3Protocol *struct { - Enabled bool `json:"enabled"` - } `json:"s3Protocol,omitempty"` - VectorBuckets *struct { - Enabled bool `json:"enabled"` - MaxBuckets int `json:"maxBuckets"` - MaxIndexes int `json:"maxIndexes"` - } `json:"vectorBuckets,omitempty"` - }{}, - } - // When local config is not set, we assume platform defaults should not change - if s.ImageTransformation != nil { - body.Features.ImageTransformation = &struct { - Enabled bool `json:"enabled"` - }{ - Enabled: s.ImageTransformation.Enabled, - } - } - // Disabling analytics and vector buckets means leaving platform values unchanged - if s.AnalyticsBuckets.Enabled { - body.Features.IcebergCatalog = &struct { - Enabled bool `json:"enabled"` - MaxCatalogs int `json:"maxCatalogs"` - MaxNamespaces int `json:"maxNamespaces"` - MaxTables int `json:"maxTables"` - }{ - Enabled: true, - MaxNamespaces: cast.UintToInt(s.AnalyticsBuckets.MaxNamespaces), - MaxTables: cast.UintToInt(s.AnalyticsBuckets.MaxTables), - MaxCatalogs: cast.UintToInt(s.AnalyticsBuckets.MaxCatalogs), - } - } - if s.VectorBuckets.Enabled { - body.Features.VectorBuckets = &struct { - Enabled bool `json:"enabled"` - MaxBuckets int `json:"maxBuckets"` - MaxIndexes int `json:"maxIndexes"` - }{ - Enabled: true, - MaxBuckets: cast.UintToInt(s.VectorBuckets.MaxBuckets), - MaxIndexes: cast.UintToInt(s.VectorBuckets.MaxIndexes), - } - } - if s.S3Protocol != nil { - body.Features.S3Protocol = &struct { - Enabled bool `json:"enabled"` - }{ - Enabled: s.S3Protocol.Enabled, - } - } - return body -} - -func (s *storage) FromRemoteStorageConfig(remoteConfig v1API.StorageConfigResponseOutput) { - s.FileSizeLimit = sizeInBytes(remoteConfig.FileSizeLimit) - if value, err := remoteConfig.MigrationVersion.Get(); err == nil { - s.TargetMigration = value - } - // When local config is not set, we assume platform defaults should not change - if s.ImageTransformation != nil { - s.ImageTransformation.Enabled = remoteConfig.Features.ImageTransformation.Enabled - } - if s.AnalyticsBuckets.Enabled { - s.AnalyticsBuckets.Enabled = remoteConfig.Features.IcebergCatalog.Enabled - s.AnalyticsBuckets.MaxNamespaces = cast.IntToUint(remoteConfig.Features.IcebergCatalog.MaxNamespaces) - s.AnalyticsBuckets.MaxTables = cast.IntToUint(remoteConfig.Features.IcebergCatalog.MaxTables) - s.AnalyticsBuckets.MaxCatalogs = cast.IntToUint(remoteConfig.Features.IcebergCatalog.MaxCatalogs) - } - if s.VectorBuckets.Enabled { - s.VectorBuckets.Enabled = remoteConfig.Features.VectorBuckets.Enabled - s.VectorBuckets.MaxBuckets = cast.IntToUint(remoteConfig.Features.VectorBuckets.MaxBuckets) - s.VectorBuckets.MaxIndexes = cast.IntToUint(remoteConfig.Features.VectorBuckets.MaxIndexes) - } - if s.S3Protocol != nil { - s.S3Protocol.Enabled = remoteConfig.Features.S3Protocol.Enabled - } -} - -func (s *storage) DiffWithRemote(remoteConfig v1API.StorageConfigResponseOutput) ([]byte, error) { - copy := s.Clone() - // Convert the config values into easily comparable remoteConfig values - currentValue, err := ToTomlBytes(copy) - if err != nil { - return nil, err - } - copy.FromRemoteStorageConfig(remoteConfig) - remoteCompare, err := ToTomlBytes(copy) - if err != nil { - return nil, err - } - return diff.Diff("remote[storage]", remoteCompare, "local[storage]", currentValue), nil -} diff --git a/apps/cli-go/pkg/config/storage_test.go b/apps/cli-go/pkg/config/storage_test.go deleted file mode 100644 index 859bb9f413..0000000000 --- a/apps/cli-go/pkg/config/storage_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package config - -import ( - "testing" - - "github.com/oapi-codegen/nullable" - "github.com/stretchr/testify/assert" - v1API "github.com/supabase/cli/pkg/api" -) - -// TargetMigration carries `toml:"-" json:"-"`, so DiffWithRemote cannot observe -// it: the clone it compares is serialized to TOML. These assert the mapping -// directly instead. -func TestStorageMigrationVersionMapping(t *testing.T) { - t.Run("adopts the remote migration version when the platform reports one", func(t *testing.T) { - s := storage{TargetMigration: "local-metadata"} - s.FromRemoteStorageConfig(v1API.StorageConfigResponseOutput{ - MigrationVersion: nullable.NewNullableWithValue("remote-metadata"), - }) - assert.Equal(t, "remote-metadata", s.TargetMigration) - }) - - t.Run("keeps the local migration version when the platform reports null", func(t *testing.T) { - s := storage{TargetMigration: "local-metadata"} - s.FromRemoteStorageConfig(v1API.StorageConfigResponseOutput{ - MigrationVersion: nullable.NewNullNullable[string](), - }) - assert.Equal(t, "local-metadata", s.TargetMigration) - }) - - t.Run("keeps the local migration version when the platform omits the field", func(t *testing.T) { - s := storage{TargetMigration: "local-metadata"} - s.FromRemoteStorageConfig(v1API.StorageConfigResponseOutput{}) - assert.Equal(t, "local-metadata", s.TargetMigration) - }) -} diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile deleted file mode 100644 index 40285169d4..0000000000 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ /dev/null @@ -1,47 +0,0 @@ -# The tag pinned below is generated from packages/stack/src/Artifacts.ts by -# apps/cli/scripts/render-service-dockerfile.ts, for these aliases only: pg, pg15, mailpit, -# postgrest, pgmeta, studio, imgproxy, edgeruntime, vector, supavisor, gotrue, realtime, storage, -# logflare. Do not hand-edit those tags; a CI drift check enforces this. Run the generator after -# a catalog update. Everything else in this file (this comment, kong, pg14, the one-shot job -# images) is hand- or Dependabot-managed. Keep a line between image lines: git conflicts on edits to -# adjacent lines, and catalog PRs for neighbouring aliases are often open at the same time. -FROM supabase/postgres:17.11.0.011 AS pg - -FROM supabase/postgres:15.19.0.011 AS pg15 -# Postgres 14 has no slim build; bumped by hand (Dependabot ignores supabase/postgres). -FROM supabase/postgres:14.1.0.89 AS pg14 -# New always-on service alias: extend SERVICE_IMAGE_ALIASES in -# shared/services/services.shared.ts and DOCKERFILE_ALIAS_BY_SERVICE in -# commands/start/start.gates.ts. -FROM library/kong:2.8.1 AS kong - -FROM axllent/mailpit:v1.31.3 AS mailpit - -FROM postgrest/postgrest:v16.5 AS postgrest - -FROM supabase/postgres-meta:v0.100.0 AS pgmeta - -FROM supabase/studio:2026.10.26-sha-478bd88 AS studio - -FROM darthsim/imgproxy:v3.26.0 AS imgproxy - -FROM supabase/edge-runtime:v1.77.9 AS edgeruntime - -FROM timberio/vector:0.58.0-alpine AS vector - -FROM supabase/supavisor:2.9.13 AS supavisor - -FROM supabase/gotrue:v2.197.0 AS gotrue - -FROM supabase/realtime:v2.149.2 AS realtime - -FROM supabase/storage-api:v1.84.36 AS storage - -FROM supabase/logflare:1.50.15 AS logflare -# One-shot job images (not started as long-running containers); differ's alias -# is resolved via dockerfileServiceImage("differ") in commands/db/diff/pgadmin-diff.ts. -FROM supabase/pgadmin-schema-diff:cli-0.0.5 AS differ - -FROM supabase/migra:3.0.1663481299 AS migra - -FROM supabase/pg_prove:3.36 AS pgprove diff --git a/apps/cli-go/pkg/config/templates/certs/kong.local.crt b/apps/cli-go/pkg/config/templates/certs/kong.local.crt deleted file mode 100644 index 0a7b2dc53c..0000000000 --- a/apps/cli-go/pkg/config/templates/certs/kong.local.crt +++ /dev/null @@ -1,30 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFJTCCAw2gAwIBAgIURUQAFIOhttOg0D12xZvx+pScX4cwDQYJKoZIhvcNAQEL -BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDcwNTE0MjI1NFoXDTM0MDcw -MzE0MjI1NFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF -AAOCAg8AMIICCgKCAgEAyL79RrCl78/XutDm7tVNyIJNZqbNSfX+3AsnP1dQc37Q -7QkxmNJ2EmfklTLyvY1A+CY/o+3S1blK+O8lm40yTHP+GC30InKS7lSnCUNqSrHy -DaGNShtphqJoao9vo69/anI6I2tKc8VQ2tebvMVzmjrsvGtWcU4ciyzbUterKQas -Iin8Aw5gDCLswVNZnl6wUvlcgbLmiTLPaJKnNsbYcBJxST2SFxY5q5YWzlugm9Q0 -3913XB64rVoKAyQ86Yabl4UXyVXknyJcbpfnsmn6ORxnZJebnuU0O9y8ys/E9hGw -5rkVTm7QjzsbczMYOgvpPbR1miyDQaZ1bGyj9SmJo1H5e12xO0RNi+awuMnpt3cB -8NeTsZctOgnEQB1Gidq2XcYV48e2OGJTNtVVVPu1UKCNeJur1AfLNPRz8fJq06A+ -Xy5xeRNGFsaeYRt/IO+/CZUOHcY/vR55i8PeeTj9cB1f9KjfmQVeOt25widxrhFK -RkpRgA1ofQ4O8/gPSOcnroQgSs2Whr9RXIF71uSLJyZU0IBxru0cznuEpXxkh7oK -Fb6t/n1SZB2WEQXHh5iG49fw7MT29CzNpJIP1ApT3LKasHZHf4yBn9ZwVAagCGSL -W7kMDhIUhOfVwpyfkXYHzQE2aaCdQ9IfZy9ybO/Y8sbUg6B5i5t8xODutv+fmd0C -AwEAAaNvMG0wHQYDVR0OBBYEFGtw+BogdJ6GJRCe+BHGQdA0f85XMB8GA1UdIwQY -MBaAFGtw+BogdJ6GJRCe+BHGQdA0f85XMA8GA1UdEwEB/wQFMAMBAf8wGgYDVR0R -BBMwEYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3DQEBCwUAA4ICAQCO9ITlkXil -+dLTddpSq06UuRTYLx8vWGLnd8uEoUg4qlLKi/hKnqTEP9dePw+QeuIM+aLjvvXY -bP4SqBOUi192OohQs7ZPCpjXizmabDNl4/Z+qyiGvZK/N0gWPEaTn6tzeQ03hECK -vY8lRrAP7xLrEYmA6unjfaj3inNUJeqL26i3SIAq/koK4jPm1F/AzwslNH00MJld -xKormGzvNJtaZPr5xrtuLQAmeCotE6av+R5k9zmSYa25Nel50JTk31fNiuUFmECD -qop+Yqfzw9/UkObDWw+g1+N3uHlH9CB2RvfkB2fW9SFMsznjQlq+MyQCwKFs699Q -rNMeHbUEwovDEcPUaLc3d5gCk/dvH9aR8mVLEcs7lMyHSsiG1Co1QXg6UjpMCrIV -Tvk+MrSQVVxw0rKS4H7ZEhfC2PfEVe1az24HrrPWJDZKnRhv1Ohj4jsxkBSprXsu -3qxcMCr7yr4hjsu9BsKtmvRfHTrMOwY1WtZBcl7hjgAItfMs29Fd4Tkn0aZs5f+n -GSwG5BeJL3kfEKwOjw8j+EAqRBu/7mbcoUyErnuLK7FimU4jtL8VX03n6nTgLJzS -xjUTaplgerrazAvV31vMaiINoaGM0RDnGjSCgZT4Va33noqo3qVJJOnWpheFmaXq -bUMQtwrAWTt12NECJt1nWAT4VWhfDoBE7A== ------END CERTIFICATE----- diff --git a/apps/cli-go/pkg/config/templates/certs/kong.local.key b/apps/cli-go/pkg/config/templates/certs/kong.local.key deleted file mode 100644 index 9b71aab612..0000000000 --- a/apps/cli-go/pkg/config/templates/certs/kong.local.key +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDIvv1GsKXvz9e6 -0Obu1U3Igk1mps1J9f7cCyc/V1BzftDtCTGY0nYSZ+SVMvK9jUD4Jj+j7dLVuUr4 -7yWbjTJMc/4YLfQicpLuVKcJQ2pKsfINoY1KG2mGomhqj2+jr39qcjoja0pzxVDa -15u8xXOaOuy8a1ZxThyLLNtS16spBqwiKfwDDmAMIuzBU1meXrBS+VyBsuaJMs9o -kqc2xthwEnFJPZIXFjmrlhbOW6Cb1DTf3XdcHritWgoDJDzphpuXhRfJVeSfIlxu -l+eyafo5HGdkl5ue5TQ73LzKz8T2EbDmuRVObtCPOxtzMxg6C+k9tHWaLINBpnVs -bKP1KYmjUfl7XbE7RE2L5rC4yem3dwHw15Oxly06CcRAHUaJ2rZdxhXjx7Y4YlM2 -1VVU+7VQoI14m6vUB8s09HPx8mrToD5fLnF5E0YWxp5hG38g778JlQ4dxj+9HnmL -w955OP1wHV/0qN+ZBV463bnCJ3GuEUpGSlGADWh9Dg7z+A9I5yeuhCBKzZaGv1Fc -gXvW5IsnJlTQgHGu7RzOe4SlfGSHugoVvq3+fVJkHZYRBceHmIbj1/DsxPb0LM2k -kg/UClPcspqwdkd/jIGf1nBUBqAIZItbuQwOEhSE59XCnJ+RdgfNATZpoJ1D0h9n -L3Js79jyxtSDoHmLm3zE4O62/5+Z3QIDAQABAoICAAnou5HYuGgtB0YWd7/EUbGb -FP0DMND8zpbICijrQ3JgCSoaMRONF/zymervd+H5bgKRyMRrweOA4P2GuIGEJ750 -X0+MPSfSJgSTsycW59FGFV+s4M+OqNfXfnX8HJOk3xI/DzWeXy91xvb2e56G1J0B -WZw3ZC31oB0SmsTtFmrRBIAvOFxiQkV3F963IY5auDNwUaupTA3rrydHTe+7QwXN -M0BdRK/VDgW1Q+ztwvDOOcnvIawvbAhKkIH1MPiKB3YkQLdUgROF27At2WcKKirV -hxkFDs56G+j0jXEpblyDJQtOiYm0mKDpVwKJvCFpYxsDpGZ22gfMkrFGb5VzdnK9 -6iMyz6eGCKBPd0I66nynZSfZwf+E3n0kGruBAEkTtGztyBxa1EUNul6uzv+Jw3No -o6cb5S7d/+4gFe6g1GoLMuWwtZyC7uXm0aZa64VgY713qkr1dDa5VHjLSs9+o2Jy -LeV6t/9/gcSn7uqtT6kQomYtpYVuEevKt/Bwj/r9Lc+KfPwOUsl5h0zUMw7c+1cL -NfCLadH1lpjB7AIiZwmKyGII4yiiIHSCZU8AKizJUvkW4TaPbtBhLkK6YYOJM+CY -DondkhlnyINaDKyH247Bfl0Ju2yl7IS3s3KUHZGbK1dBc5wC1jW7cp7yA9ZGxk8X -+5NIuozgp6Qd0swh/7XVAoIBAQD3wjJdMgqiuhveVY1tOpKa9mwdW/dSyY421Dvi -XUOEhgWPyheu78aahF3qrhY5yqX4oh643R4qQrok+7wphigLelMboqqb9qM/NL8R -m4jLvlHygHUQhrF8aGjRAKzfnOU0djpCLK1TJrvkxdZfo1ngzxfE9vo4OwtLcRuZ -GMWZNTthUyXEIkw6e4yoJvpk+cb7+TvTnVMjuks5tqI4OgXQ0fYmRixImRg/6PTC -QgsbG3FFxs71vrVJfcFmeymjq0gXHnviMDAgbqjnnHdqhsIauA0cByBfAQWrujUK -N9sTy/Fwni3j8Lxin5HxP8cZxkr2p7ynG98sfGMZFoqGn8JLAoIBAQDPbHRfJz69 -5Os82zumYXfNQw0pfiKn8Jve+dw1i/gSzj8wb98sRZ9w9scY5KBFH1tG5UcWdYtv -UJnUc1OO4mKmVpzrsQjS/qsGTPj2WkXEy3icoyJcDMFw6LBhctNHBwjtKL8Fe+ez -EPHR+Sy7WuvJphZhm817VQXFK5AAmfYJbraI7B9Qm7uP7o1+pv94+886BxWi6Kqm -wlEz+YhCQNPol5Lzth9/a3Ty0cwkaF5hG844Zu+feNWPdT7+nPs4QMWtCuQXD7cY -/UVjyUp8R5lL35woVqTyh2DUbawnAgvy5VR/q6nvnnorBSS5CCEokFTbcC2+5g2s -cquTixmnnzt3AoIBAEEAOApvWI782NbqByLDHTd+szq4qBiWPLNx1ww6f75DEy54 -TOJekfJ0C9fIOJiM7CJhFkJV8NYRj5ioWuNiCRuJykgT00L74DRy+DX8YlVh50oC -HbRqQiJrmiRLUw+PsCqlYf9+jTKllOIebXHF/4yViJzqVxdtljcCw8TU1PKvHpcY -I25juTAWvf5Xy5W2Wlg9OU75lZhoBdhsa/7oaiIxE61S98dMMyqBScW1YgiImJ0/ -3YhnslRHo/fpFDGWfGFkZAghsF6x6apqSIIwPLiZ1Qanb/y83eoB/mxh3x5AgWWU -emG1BRey2a07mFbQyVYRB9On4lNfhvhMC6fqPiUCggEBAIWznv+GyIaW/JrppfWF -dje1NNK+H5CpI5AC9tL9OhbFIy4RroVXC4NBTuzY0gY8RBXazIvJkOtbxQyQmhfM -DVKzGctC/jEjPxJ8oy4I2lgT39b3wLoc7sPc+XSXE2EBQ9u7HAZo690c2wNB3neW -D86XZG3ehAEvuNxzTAtXMqS53Kc9rKHFAI2yczpkYuCVT+pTjrrHxthfAQDDo/Uw -3Gdc7LhzeLIljejHGxOMwulNhwwMLgFZXi9uoAB3J+gGuGTmVEgZNSjCeOvtYDnj -3QhvRrUpxIEgimXjm8g5JYKYGHODL7LpME5yrk0m1FjVUB1yncojeETkVXxxeUP2 -+9kCggEBAMcsbVgvNVwt7bBf1zuw/AiXX4e3Lc2019M/z9SFsu6WfA+OgNMM90lQ -Eqq6XGhlNSDw+Zz8JDPq5RIM3rGlKFWAFSQ+h/4q67LgIDTOKjk7EXpbt/bJIFud -xyiG6uvMSX35OZ5vLsBvv2AzkzR5x2v9kcKD1b5eWn6haEXMOX/X0yGHux9vRylw -zEW1IOA5+2FZvZhwLy+F3XxY3Vrf04mWGyCxiBy4aMvbmp0BcWBg+tRLOmc6h7rX -2Bi8TfeE+GnmJkezD44MH/S+EEfafOkiDqoDCxxgYpJzXiE6DZNEUGdpUm87hdx4 -MviBG5FCtAAJp+5btxy72dIKBYBVUJ8= ------END PRIVATE KEY----- diff --git a/apps/cli-go/pkg/config/testdata/TestApiDiff/detects_differences.diff b/apps/cli-go/pkg/config/testdata/TestApiDiff/detects_differences.diff deleted file mode 100644 index 5d0e9603ef..0000000000 --- a/apps/cli-go/pkg/config/testdata/TestApiDiff/detects_differences.diff +++ /dev/null @@ -1,14 +0,0 @@ -diff remote[api] local[api] ---- remote[api] -+++ local[api] -@@ -1,7 +1,7 @@ - enabled = true --schemas = ["public"] --extra_search_path = ["public"] --max_rows = 500 -+schemas = ["public", "private"] -+extra_search_path = ["extensions", "public"] -+max_rows = 1000 - port = 0 - external_url = "" - diff --git a/apps/cli-go/pkg/config/testdata/TestApiDiff/handles_api_disabled_on_local_side.diff b/apps/cli-go/pkg/config/testdata/TestApiDiff/handles_api_disabled_on_local_side.diff deleted file mode 100644 index 67a5ccd5ca..0000000000 --- a/apps/cli-go/pkg/config/testdata/TestApiDiff/handles_api_disabled_on_local_side.diff +++ /dev/null @@ -1,9 +0,0 @@ -diff remote[api] local[api] ---- remote[api] -+++ local[api] -@@ -1,4 +1,4 @@ --enabled = true -+enabled = false - schemas = ["public"] - extra_search_path = ["public"] - max_rows = 500 diff --git a/apps/cli-go/pkg/config/testdata/TestApiDiff/handles_api_disabled_on_remote_side.diff b/apps/cli-go/pkg/config/testdata/TestApiDiff/handles_api_disabled_on_remote_side.diff deleted file mode 100644 index 200d36c403..0000000000 --- a/apps/cli-go/pkg/config/testdata/TestApiDiff/handles_api_disabled_on_remote_side.diff +++ /dev/null @@ -1,9 +0,0 @@ -diff remote[api] local[api] ---- remote[api] -+++ local[api] -@@ -1,4 +1,4 @@ --enabled = false -+enabled = true - schemas = ["public", "private"] - extra_search_path = ["extensions", "public"] - max_rows = 500 diff --git a/apps/cli-go/pkg/config/testdata/TestAuthDiff/local_enabled_and_disabled.diff b/apps/cli-go/pkg/config/testdata/TestAuthDiff/local_enabled_and_disabled.diff deleted file mode 100644 index 21094fdd40..0000000000 --- a/apps/cli-go/pkg/config/testdata/TestAuthDiff/local_enabled_and_disabled.diff +++ /dev/null @@ -1,30 +0,0 @@ -diff remote[auth] local[auth] ---- remote[auth] -+++ local[auth] -@@ -1,16 +1,16 @@ - enabled = false --site_url = "" -+site_url = "http://127.0.0.1:3000" - external_url = "" --additional_redirect_urls = ["https://127.0.0.1:3000", "https://ref.supabase.co"] --jwt_expiry = 0 -+additional_redirect_urls = ["https://127.0.0.1:3000"] -+jwt_expiry = 3600 - jwt_issuer = "" --enable_refresh_token_rotation = true --refresh_token_reuse_interval = 0 --enable_manual_linking = true --enable_signup = true --enable_anonymous_sign_ins = true --minimum_password_length = 8 --password_requirements = "letters_digits" -+enable_refresh_token_rotation = false -+refresh_token_reuse_interval = 10 -+enable_manual_linking = false -+enable_signup = false -+enable_anonymous_sign_ins = false -+minimum_password_length = 6 -+password_requirements = "lower_upper_letters_digits_symbols" - signing_keys_path = "" - publishable_key = "" - secret_key = "" diff --git a/apps/cli-go/pkg/config/testdata/TestCaptchaDiff/local_disabled_remote_enabled.diff b/apps/cli-go/pkg/config/testdata/TestCaptchaDiff/local_disabled_remote_enabled.diff deleted file mode 100644 index 2df3d18a24..0000000000 --- a/apps/cli-go/pkg/config/testdata/TestCaptchaDiff/local_disabled_remote_enabled.diff +++ /dev/null @@ -1,12 +0,0 @@ -diff remote[auth] local[auth] ---- remote[auth] -+++ local[auth] -@@ -28,7 +28,7 @@ - web3 = 0 - - [captcha] --enabled = true -+enabled = false - provider = "turnstile" - secret = "hash:ed64b7695a606bc6ab4fcb41fe815b5ddf1063ccbc87afe1fa89756635db520e" - diff --git a/apps/cli-go/pkg/config/testdata/TestCaptchaDiff/local_enabled_remote_disabled.diff b/apps/cli-go/pkg/config/testdata/TestCaptchaDiff/local_enabled_remote_disabled.diff deleted file mode 100644 index ea98e88092..0000000000 --- a/apps/cli-go/pkg/config/testdata/TestCaptchaDiff/local_enabled_remote_disabled.diff +++ /dev/null @@ -1,16 +0,0 @@ -diff remote[auth] local[auth] ---- remote[auth] -+++ local[auth] -@@ -28,9 +28,9 @@ - web3 = 0 - - [captcha] --enabled = false --provider = "hcaptcha" --secret = "hash:ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252" -+enabled = true -+provider = "turnstile" -+secret = "hash:ed64b7695a606bc6ab4fcb41fe815b5ddf1063ccbc87afe1fa89756635db520e" - - [hook] - diff --git a/apps/cli-go/pkg/config/testdata/TestDbSettingsDiff/detects_differences.diff b/apps/cli-go/pkg/config/testdata/TestDbSettingsDiff/detects_differences.diff deleted file mode 100644 index 12aeb50ae3..0000000000 --- a/apps/cli-go/pkg/config/testdata/TestDbSettingsDiff/detects_differences.diff +++ /dev/null @@ -1,10 +0,0 @@ -diff remote[db.settings] local[db.settings] ---- remote[db.settings] -+++ local[db.settings] -@@ -1,3 +1,3 @@ --effective_cache_size = "8GB" --max_connections = 200 --shared_buffers = "2GB" -+effective_cache_size = "4GB" -+max_connections = 100 -+shared_buffers = "1GB" diff --git a/apps/cli-go/pkg/config/testdata/TestDbSettingsDiff/handles_api_disabled_on_local_side.diff b/apps/cli-go/pkg/config/testdata/TestDbSettingsDiff/handles_api_disabled_on_local_side.diff deleted file mode 100644 index 392143fd13..0000000000 --- a/apps/cli-go/pkg/config/testdata/TestDbSettingsDiff/handles_api_disabled_on_local_side.diff +++ /dev/null @@ -1,7 +0,0 @@ -diff remote[db.settings] local[db.settings] ---- remote[db.settings] -+++ local[db.settings] -@@ -1,3 +0,0 @@ --effective_cache_size = "4GB" --max_connections = 100 --shared_buffers = "1GB" diff --git a/apps/cli-go/pkg/config/testdata/TestDbSettingsDiff/handles_api_disabled_on_remote_side.diff b/apps/cli-go/pkg/config/testdata/TestDbSettingsDiff/handles_api_disabled_on_remote_side.diff deleted file mode 100644 index 9382425387..0000000000 --- a/apps/cli-go/pkg/config/testdata/TestDbSettingsDiff/handles_api_disabled_on_remote_side.diff +++ /dev/null @@ -1,7 +0,0 @@ -diff remote[db.settings] local[db.settings] ---- remote[db.settings] -+++ local[db.settings] -@@ -0,0 +1,3 @@ -+effective_cache_size = "4GB" -+max_connections = 100 -+shared_buffers = "1GB" diff --git a/apps/cli-go/pkg/config/testdata/TestEmailDiff/local_disabled_remote_enabled.diff b/apps/cli-go/pkg/config/testdata/TestEmailDiff/local_disabled_remote_enabled.diff deleted file mode 100644 index d341bab3f4..0000000000 --- a/apps/cli-go/pkg/config/testdata/TestEmailDiff/local_disabled_remote_enabled.diff +++ /dev/null @@ -1,57 +0,0 @@ -diff remote[auth] local[auth] ---- remote[auth] -+++ local[auth] -@@ -49,13 +49,13 @@ - inactivity_timeout = "0s" - - [email] --enable_signup = true --double_confirm_changes = true --enable_confirmations = true --secure_password_change = true --max_frequency = "1s" --otp_length = 6 --otp_expiry = 3600 -+enable_signup = false -+double_confirm_changes = false -+enable_confirmations = false -+secure_password_change = false -+max_frequency = "1m0s" -+otp_length = 8 -+otp_expiry = 86400 - [email.template] - [email.template.confirmation] - content_path = "" -@@ -71,25 +71,25 @@ - content_path = "" - [email.notification] - [email.notification.email_changed] --enabled = true -+enabled = false - content_path = "" - [email.notification.identity_linked] --enabled = true -+enabled = false - content_path = "" - [email.notification.identity_unlinked] --enabled = true -+enabled = false - content_path = "" - [email.notification.mfa_factor_enrolled] --enabled = true -+enabled = false - content_path = "" - [email.notification.mfa_factor_unenrolled] --enabled = true -+enabled = false - content_path = "" - [email.notification.password_changed] --enabled = true -+enabled = false - content_path = "" - [email.notification.phone_changed] --enabled = true -+enabled = false - content_path = "" - - [sms] diff --git a/apps/cli-go/pkg/config/testdata/TestEmailDiff/local_enabled_remote_disabled.diff b/apps/cli-go/pkg/config/testdata/TestEmailDiff/local_enabled_remote_disabled.diff deleted file mode 100644 index c3a49ddf02..0000000000 --- a/apps/cli-go/pkg/config/testdata/TestEmailDiff/local_enabled_remote_disabled.diff +++ /dev/null @@ -1,99 +0,0 @@ -diff remote[auth] local[auth] ---- remote[auth] -+++ local[auth] -@@ -49,62 +49,74 @@ - inactivity_timeout = "0s" - - [email] --enable_signup = false --double_confirm_changes = false --enable_confirmations = false --secure_password_change = false --max_frequency = "1m0s" --otp_length = 6 --otp_expiry = 3600 -+enable_signup = true -+double_confirm_changes = true -+enable_confirmations = true -+secure_password_change = true -+max_frequency = "1s" -+otp_length = 8 -+otp_expiry = 86400 - [email.template] - [email.template.confirmation] -+subject = "confirmation-subject" - content_path = "" - [email.template.email_change] -+subject = "email-change-subject" - content = "email-change-content" - content_path = "" - [email.template.invite] -+subject = "invite-subject" -+content = "invite-content" - content_path = "" - [email.template.magic_link] - subject = "magic-link-subject" -+content = "magic-link-content" - content_path = "" - [email.template.reauthentication] -+subject = "" -+content = "" - content_path = "" - [email.template.recovery] -+content = "recovery-content" - content_path = "" - [email.notification] - [email.notification.email_changed] --enabled = false -+enabled = true - subject = "email-changed-subject" -+content = "email-changed-content" - content_path = "" - [email.notification.identity_linked] --enabled = false -+enabled = true -+subject = "identity-linked-subject" -+content = "identity-linked-content" - content_path = "" - [email.notification.identity_unlinked] --enabled = false -+enabled = true - content_path = "" - [email.notification.mfa_factor_enrolled] --enabled = false -+enabled = true - content_path = "" - [email.notification.mfa_factor_unenrolled] --enabled = false -+enabled = true - content_path = "" - [email.notification.password_changed] --enabled = false -+enabled = true - subject = "password-changed-subject" - content = "password-changed-content" - content_path = "" - [email.notification.phone_changed] --enabled = false -+enabled = true -+subject = "phone-changed-subject" - content = "phone-changed-content" - content_path = "" - [email.smtp] --enabled = false --host = "" --port = 0 --user = "" --pass = "" --admin_email = "" --sender_name = "" -+enabled = true -+host = "smtp.sendgrid.net" -+port = 587 -+user = "apikey" -+pass = "hash:ed64b7695a606bc6ab4fcb41fe815b5ddf1063ccbc87afe1fa89756635db520e" -+admin_email = "admin@email.com" -+sender_name = "Admin" - - [sms] - enable_signup = false diff --git a/apps/cli-go/pkg/config/testdata/TestExternalDiff/local_enabled_and_disabled.diff b/apps/cli-go/pkg/config/testdata/TestExternalDiff/local_enabled_and_disabled.diff deleted file mode 100644 index 54ca03e506..0000000000 --- a/apps/cli-go/pkg/config/testdata/TestExternalDiff/local_enabled_and_disabled.diff +++ /dev/null @@ -1,34 +0,0 @@ -diff remote[auth] local[auth] ---- remote[auth] -+++ local[auth] -@@ -89,7 +89,7 @@ - - [external] - [external.apple] --enabled = false -+enabled = true - client_id = "test-client-1,test-client-2" - secret = "hash:ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252" - url = "" -@@ -97,9 +97,9 @@ - skip_nonce_check = false - email_optional = false - [external.azure] --enabled = false --client_id = "" --secret = "" -+enabled = true -+client_id = "test-client-1" -+secret = "hash:ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252" - url = "" - redirect_uri = "" - skip_nonce_check = false -@@ -153,7 +153,7 @@ - skip_nonce_check = false - email_optional = false - [external.google] --enabled = true -+enabled = false - client_id = "test-client-2" - secret = "" - url = "" diff --git a/apps/cli-go/pkg/config/testdata/TestHookDiff/local_disabled_remote_enabled.diff b/apps/cli-go/pkg/config/testdata/TestHookDiff/local_disabled_remote_enabled.diff deleted file mode 100644 index d0b4dcc30e..0000000000 --- a/apps/cli-go/pkg/config/testdata/TestHookDiff/local_disabled_remote_enabled.diff +++ /dev/null @@ -1,32 +0,0 @@ -diff remote[auth] local[auth] ---- remote[auth] -+++ local[auth] -@@ -29,23 +29,23 @@ - - [hook] - [hook.mfa_verification_attempt] --enabled = true -+enabled = false - uri = "pg-functions://postgres/public/verifyMFA" - secrets = "" - [hook.custom_access_token] --enabled = true -+enabled = false - uri = "" - secrets = "" - [hook.send_sms] --enabled = true -+enabled = false - uri = "https://example.com" - secrets = "" - [hook.send_email] --enabled = true -+enabled = false - uri = "" - secrets = "" - [hook.before_user_created] --enabled = true -+enabled = false - uri = "" - secrets = "" - diff --git a/apps/cli-go/pkg/config/testdata/TestHookDiff/local_enabled_remote_disabled.diff b/apps/cli-go/pkg/config/testdata/TestHookDiff/local_enabled_remote_disabled.diff deleted file mode 100644 index 3c3364d17b..0000000000 --- a/apps/cli-go/pkg/config/testdata/TestHookDiff/local_enabled_remote_disabled.diff +++ /dev/null @@ -1,39 +0,0 @@ -diff remote[auth] local[auth] ---- remote[auth] -+++ local[auth] -@@ -29,25 +29,25 @@ - - [hook] - [hook.mfa_verification_attempt] --enabled = false -+enabled = true - uri = "pg-functions://postgres/public/verifyMFA" - secrets = "" - [hook.custom_access_token] --enabled = false --uri = "pg-functions://postgres/public/customToken" --secrets = "" -+enabled = true -+uri = "http://example.com" -+secrets = "hash:ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252" - [hook.send_sms] --enabled = false -+enabled = true - uri = "https://example.com" - secrets = "hash:ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252" - [hook.send_email] --enabled = false --uri = "https://example.com" -+enabled = true -+uri = "pg-functions://postgres/public/sendEmail" - secrets = "" - [hook.before_user_created] --enabled = false --uri = "pg-functions://postgres/public/beforeUserCreated" --secrets = "" -+enabled = true -+uri = "http://example.com" -+secrets = "hash:ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252" - - [mfa] - max_enrolled_factors = 0 diff --git a/apps/cli-go/pkg/config/testdata/TestMfaDiff/local_enabled_and_disabled.diff b/apps/cli-go/pkg/config/testdata/TestMfaDiff/local_enabled_and_disabled.diff deleted file mode 100644 index ec1f6ebe90..0000000000 --- a/apps/cli-go/pkg/config/testdata/TestMfaDiff/local_enabled_and_disabled.diff +++ /dev/null @@ -1,26 +0,0 @@ -diff remote[auth] local[auth] ---- remote[auth] -+++ local[auth] -@@ -30,16 +30,16 @@ - [hook] - - [mfa] --max_enrolled_factors = 10 -+max_enrolled_factors = 0 - [mfa.totp] - enroll_enabled = false - verify_enabled = false - [mfa.phone] --enroll_enabled = false --verify_enabled = false --otp_length = 6 --template = "Your code is {{ .Code }}" --max_frequency = "5s" -+enroll_enabled = true -+verify_enabled = true -+otp_length = 0 -+template = "" -+max_frequency = "0s" - [mfa.web_authn] - enroll_enabled = false - verify_enabled = false diff --git a/apps/cli-go/pkg/config/testdata/TestRateLimitsDiff/local_and_remote_rate_limits_differ.diff b/apps/cli-go/pkg/config/testdata/TestRateLimitsDiff/local_and_remote_rate_limits_differ.diff deleted file mode 100644 index 9663c1438b..0000000000 --- a/apps/cli-go/pkg/config/testdata/TestRateLimitsDiff/local_and_remote_rate_limits_differ.diff +++ /dev/null @@ -1,20 +0,0 @@ -diff remote[auth] local[auth] ---- remote[auth] -+++ local[auth] -@@ -19,12 +19,12 @@ - service_role_key = "" - - [rate_limit] --anonymous_users = 10 -+anonymous_users = 20 - token_refresh = 30 --sign_in_sign_ups = 45 -+sign_in_sign_ups = 40 - token_verifications = 50 --email_sent = 15 --sms_sent = 55 -+email_sent = 25 -+sms_sent = 35 - web3 = 0 - - [hook] diff --git a/apps/cli-go/pkg/config/testdata/TestSmsDiff/enable_sign_up_without_provider.diff b/apps/cli-go/pkg/config/testdata/TestSmsDiff/enable_sign_up_without_provider.diff deleted file mode 100644 index 2e44496fd3..0000000000 --- a/apps/cli-go/pkg/config/testdata/TestSmsDiff/enable_sign_up_without_provider.diff +++ /dev/null @@ -1,12 +0,0 @@ -diff remote[auth] local[auth] ---- remote[auth] -+++ local[auth] -@@ -58,7 +58,7 @@ - otp_expiry = 0 - - [sms] --enable_signup = false -+enable_signup = true - enable_confirmations = false - template = "" - max_frequency = "0s" diff --git a/apps/cli-go/pkg/config/testdata/TestSmsDiff/local_disabled_remote_enabled.diff b/apps/cli-go/pkg/config/testdata/TestSmsDiff/local_disabled_remote_enabled.diff deleted file mode 100644 index 385f49fb83..0000000000 --- a/apps/cli-go/pkg/config/testdata/TestSmsDiff/local_disabled_remote_enabled.diff +++ /dev/null @@ -1,30 +0,0 @@ -diff remote[auth] local[auth] ---- remote[auth] -+++ local[auth] -@@ -58,12 +58,12 @@ - otp_expiry = 0 - - [sms] --enable_signup = true --enable_confirmations = true --template = "Your code is {{ .Code }}" --max_frequency = "1m0s" -+enable_signup = false -+enable_confirmations = false -+template = "" -+max_frequency = "0s" - [sms.twilio] --enabled = true -+enabled = false - account_sid = "" - message_service_sid = "" - auth_token = "" -@@ -86,8 +86,6 @@ - api_key = "" - api_secret = "" - [sms.test_otp] --123 = "456" --456 = "123" - - [web3] - [web3.solana] diff --git a/apps/cli-go/pkg/config/testdata/TestSmsDiff/local_enabled_remote_disabled.diff b/apps/cli-go/pkg/config/testdata/TestSmsDiff/local_enabled_remote_disabled.diff deleted file mode 100644 index 3db44eabab..0000000000 --- a/apps/cli-go/pkg/config/testdata/TestSmsDiff/local_enabled_remote_disabled.diff +++ /dev/null @@ -1,42 +0,0 @@ -diff remote[auth] local[auth] ---- remote[auth] -+++ local[auth] -@@ -58,12 +58,12 @@ - otp_expiry = 0 - - [sms] --enable_signup = false --enable_confirmations = false --template = "" --max_frequency = "0s" -+enable_signup = true -+enable_confirmations = true -+template = "Your code is {{ .Code }}" -+max_frequency = "1m0s" - [sms.twilio] --enabled = true -+enabled = false - account_sid = "" - message_service_sid = "" - auth_token = "" -@@ -73,9 +73,9 @@ - message_service_sid = "" - auth_token = "" - [sms.messagebird] --enabled = false --originator = "" --access_key = "" -+enabled = true -+originator = "test-originator" -+access_key = "hash:ab60d03fc809fb02dae838582f3ddc13d1d6cb32ffba77c4b969dd3caa496f13" - [sms.textlocal] - enabled = false - sender = "" -@@ -86,6 +86,7 @@ - api_key = "" - api_secret = "" - [sms.test_otp] -+123 = "456" - - [web3] - [web3.solana] diff --git a/apps/cli-go/pkg/config/testdata/config.toml b/apps/cli-go/pkg/config/testdata/config.toml deleted file mode 100644 index 4c6ffc0c80..0000000000 --- a/apps/cli-go/pkg/config/testdata/config.toml +++ /dev/null @@ -1,378 +0,0 @@ -# For detailed configuration reference documentation, visit: -# https://supabase.com/docs/guides/local-development/cli/config -# A string used to distinguish different Supabase projects on the same host. Defaults to the -# working directory name when running `supabase init`. -project_id = "test" - -[api] -enabled = true -# Port to use for the API URL. -port = 54321 -# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API -# endpoints. `public` and `graphql_public` schemas are included by default. -schemas = ["public", "graphql_public"] -# Extra schemas to add to the search_path of every request. public is always included. -extra_search_path = ["public", "extensions"] -# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size -# for accidental or malicious requests. -max_rows = 1000 - -[api.tls] -# Enable HTTPS endpoints locally using a self-signed certificate. -enabled = true -# Paths to self-signed certificate pair. -cert_path = "../certs/my-cert.pem" -key_path = "../certs/my-key.pem" - -[db] -# Port to use for the local database URL. -port = 54322 -# Port used by db diff command to initialize the shadow database. -shadow_port = 54320 -# Maximum amount of time to wait for health check when starting the local database. -health_timeout = "2m" -# The database major version to use. This has to be the same as your remote database's. Run `SHOW -# server_version;` on the remote database to check. -major_version = 17 -orioledb_version = "15.1.0.150" - -[db.migrations] -# If disabled, migrations will be skipped during a db push or reset. -enabled = true -# Specifies an ordered list of schema files that describe your database. -# Supports glob patterns relative to supabase directory: "./schemas/*.sql" -schema_paths = ["./schemas/*.sql"] - -[db.pooler] -enabled = true -# Port to use for the local connection pooler. -port = 54329 -# Specifies when a server connection can be reused by other clients. -# Configure one of the supported pooler modes: `transaction`, `session`. -pool_mode = "transaction" -# How many server connections to allow per user/database pair. -default_pool_size = 20 -# Maximum number of client connections allowed. -max_client_conn = 100 - -[db.vault] -test_key = "test_value" - -[db.seed] -# If enabled, seeds the database after migrations during a db reset. -enabled = true -# Specifies an ordered list of seed files to load during db reset. -# Supports glob patterns relative to supabase directory: "./seeds/*.sql" -sql_paths = ["./seed.sql"] - -[db.network_restrictions] -# Enable management of network restrictions. -enabled = true -# List of IPv4 CIDR blocks allowed to connect to the database. -# Defaults to allow all IPv4 connections. Set empty array to block all IPs. -allowed_cidrs = ["0.0.0.0/0"] -# List of IPv6 CIDR blocks allowed to connect to the database. -# Defaults to allow all IPv6 connections. Set empty array to block all IPs. -allowed_cidrs_v6 = ["::/0"] - -# Uncomment to reject non-secure connections to the database. -[db.ssl_enforcement] -enabled = true - -[realtime] -enabled = true -# Bind realtime via either IPv4 or IPv6. (default: IPv6) -ip_version = "IPv4" -# The maximum length in bytes of HTTP request headers. (default: 4096) -max_header_length = 8192 - -[studio] -enabled = true -# Port to use for Supabase Studio. -port = 54323 -# External URL of the API server that frontend connects to. -api_url = "http://127.0.0.1" -# OpenAI API Key to use for Supabase AI in the Supabase Studio. -openai_api_key = "env(OPENAI_API_KEY)" - -# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they -# are monitored, and you can view the emails that would have been sent from the web interface. -[local_smtp] -enabled = true -# Port to use for the email testing server web interface. -port = 54324 -# Uncomment to expose additional ports for testing user applications that send emails. -# smtp_port = 54325 -# pop3_port = 54326 -# admin_email = "admin@email.com" -# sender_name = "Admin" - -[storage] -enabled = true -# The maximum file size allowed (e.g. "5MB", "500KB"). -file_size_limit = "50MiB" - -# Uncomment to configure local storage buckets -[storage.buckets.images] -public = false -file_size_limit = "50MiB" -allowed_mime_types = ["image/png", "image/jpeg"] -objects_path = "./images" - -# Allow connections via S3 compatible clients -[storage.s3_protocol] -enabled = true - -# Image transformation API is available to Supabase Pro plan. -[storage.image_transformation] -enabled = true - -# Store analytical data in S3 for running ETL jobs over Iceberg Catalog -[storage.analytics] -enabled = true -max_namespaces = 5 -max_tables = 10 -max_catalogs = 2 - -# Analytics Buckets is available to Supabase Pro plan. -[storage.analytics.buckets.my-warehouse] - -# Store vector embeddings in S3 for large and durable datasets -[storage.vector] -enabled = true -max_buckets = 10 -max_indexes = 5 - -# Vector Buckets is available to Supabase Pro plan. -# [storage.vector.buckets.documents-openai] - -[auth] -enabled = true -# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used -# in emails. -site_url = "http://127.0.0.1:3000" -# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. -additional_redirect_urls = ["https://127.0.0.1:3000", "env(AUTH_CALLBACK_URL)"] -# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). -jwt_expiry = 3600 -# Path to JWT signing key. DO NOT commit your signing keys file to git. -signing_keys_path = "./signing_keys.json" -# If disabled, the refresh token will never expire. -enable_refresh_token_rotation = true -# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. -# Requires enable_refresh_token_rotation = true. -refresh_token_reuse_interval = 10 -# Allow/disallow new user signups to your project. -enable_signup = true -# Allow/disallow anonymous sign-ins to your project. -enable_anonymous_sign_ins = true -# Allow/disallow testing manual linking of accounts -enable_manual_linking = true -# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. -minimum_password_length = 6 -# Passwords that do not meet the following requirements will be rejected as weak. Supported values -# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` -password_requirements = "" - -# OAuth server configuration -[auth.oauth_server] -# Enable OAuth server functionality -enabled = true -# Path for OAuth consent flow UI -authorization_url_path = "/oauth/consent" -# Allow dynamic client registration -allow_dynamic_registration = true - -[auth.rate_limit] -# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled. -email_sent = 2 -# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled. -sms_sent = 30 -# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true. -anonymous_users = 30 -# Number of sessions that can be refreshed in a 5 minute interval per IP address. -token_refresh = 150 -# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users). -sign_in_sign_ups = 30 -# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. -token_verifications = 30 -# Number of Web3 logins that can be made in a 5 minute interval per IP address. -web3 = 30 - -# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. -[auth.captcha] -enabled = true -provider = "hcaptcha" -secret = "env(HCAPTCHA_SECRET)" - -[auth.email] -# Allow/disallow new user signups via email to your project. -enable_signup = true -# If enabled, a user will be required to confirm any email change on both the old, and new email -# addresses. If disabled, only the new email is required to confirm. -double_confirm_changes = true -# If enabled, users need to confirm their email address before signing in. -enable_confirmations = false -# If enabled, users will need to reauthenticate or have logged in recently to change their password. -secure_password_change = true -# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. -max_frequency = "1s" -# Number of characters used in the email OTP. -otp_length = 6 -# Number of seconds before the email OTP expires (defaults to 1 hour). -otp_expiry = 3600 - -# Use a production-ready SMTP server -[auth.email.smtp] -enabled = true -host = "smtp.sendgrid.net" -port = 587 -user = "apikey" -pass = "env(SENDGRID_API_KEY)" -admin_email = "admin@email.com" -sender_name = "Admin" - -# Uncomment to customize email template -[auth.email.template.invite] -subject = "You have been invited" -content_path = "./supabase/templates/invite.html" - -# Uncomment to customize notification email template -[auth.email.notification.password_changed] -enabled = true -subject = "Your password has been changed" -content_path = "./templates/password_changed_notification.html" - -[auth.sms] -# Allow/disallow new user signups via SMS to your project. -enable_signup = true -# If enabled, users need to confirm their phone number before signing in. -enable_confirmations = false -# Template for sending OTP to users -template = "Your code is {{ `{{ .Code }}` }}" -# Controls the minimum amount of time that must pass before sending another sms otp. -max_frequency = "5s" - -# Use pre-defined map of phone number to OTP for testing. -[auth.sms.test_otp] -4152127777 = "123456" - -# Configure logged in session timeouts. -[auth.sessions] -# Force log out after the specified duration. -timebox = "24h" -# Force log out if the user has been inactive longer than the specified duration. -inactivity_timeout = "8h" - -# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object. -[auth.hook.before_user_created] -enabled = true -uri = "pg-functions://postgres/auth/before-user-created-hook" - -# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. -[auth.hook.custom_access_token] -enabled = true -uri = "pg-functions://postgres/auth/custom-access-token-hook" - -[auth.hook.send_sms] -enabled = true -uri = "http://host.docker.internal/functions/v1/send_sms" -secrets = "env(AUTH_SEND_SMS_SECRETS)" - -# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. -[auth.sms.twilio] -enabled = true -account_sid = "account_sid" -message_service_sid = "message_service_sid" -# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: -auth_token = "env(TWILIO_AUTH_TOKEN)" - -# Multi-factor-authentication is available to Supabase Pro plan. -[auth.mfa] -max_enrolled_factors = 10 - -# Configure MFA via App Authenticator (TOTP) -[auth.mfa.totp] -enroll_enabled = true -verify_enabled = true - -# Configure MFA via Phone Messaging -[auth.mfa.phone] -enroll_enabled = true -verify_enabled = true -otp_length = 6 -template = "Your code is {{ `{{ .Code }}` }}" -max_frequency = "5s" - -# Configure MFA via Phone Messaging -[auth.mfa.web_authn] -enroll_enabled = true -verify_enabled = true - -# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, -# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, -# `twitter`, `x`, `slack`, `spotify`, `workos`, `zoom`. -[auth.external.azure] -enabled = true -client_id = "env(AZURE_CLIENT_ID)" -secret = "env(AZURE_SECRET)" -# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, -# or any other third-party OIDC providers. -url = "https://login.microsoftonline.com/tenant" -# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. -skip_nonce_check = true -# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address. -email_optional = true - -# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard. -# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting. -[auth.web3.solana] -enabled = true - -[edge_runtime] -enabled = true -# Configure one of the supported request policies: `oneshot`, `per_worker`. -# Use `oneshot` for hot reload, or `per_worker` for load testing. -policy = "per_worker" -inspector_port = 8083 -deno_version = 2 - -[edge_runtime.secrets] -test_key = "test_value" - -[analytics] -enabled = true -port = 54327 -# Configure one of the supported backends: `postgres`, `bigquery`. -backend = "postgres" - -# Experimental features may be deprecated any time -[experimental] -# Configures S3 bucket URL, eg. .s3-.amazonaws.com -s3_host = "orioledb.s3-accelerate.amazonaws.com" -# Configures S3 bucket region, eg. us-east-1 -s3_region = "ap-southeast-1" -# Configures AWS_ACCESS_KEY_ID for S3 bucket -s3_access_key = "" -# Configures AWS_SECRET_ACCESS_KEY for S3 bucket -s3_secret_key = "" - -[remotes.production] -project_id = "vpefcjyosynxeiebfscx" - -[remotes.production.auth] -site_url = "http://feature-auth-branch.com/" -enable_signup = false - -[remotes.production.auth.external.azure] -enabled = false -client_id = "nope" - -[remotes.staging] -project_id = "bvikqvbczudanvggcord" - -[remotes.staging.db.seed] -enabled = true - -[remotes.staging.storage.buckets.images] -allowed_mime_types = ["image/png"] diff --git a/apps/cli-go/pkg/config/updater.go b/apps/cli-go/pkg/config/updater.go deleted file mode 100644 index 6c7e7eecd1..0000000000 --- a/apps/cli-go/pkg/config/updater.go +++ /dev/null @@ -1,318 +0,0 @@ -package config - -import ( - "context" - "fmt" - "os" - - "github.com/go-errors/errors" - v1API "github.com/supabase/cli/pkg/api" -) - -type ConfigUpdater struct { - client v1API.ClientWithResponses -} - -func NewConfigUpdater(client v1API.ClientWithResponses) ConfigUpdater { - return ConfigUpdater{client: client} -} - -func (u *ConfigUpdater) UpdateRemoteConfig(ctx context.Context, remote baseConfig, filter ...func(string) bool) error { - if err := u.UpdateApiConfig(ctx, remote.ProjectId, remote.Api, filter...); err != nil { - return err - } - if err := u.UpdateDbConfig(ctx, remote.ProjectId, remote.Db, filter...); err != nil { - return err - } - if err := u.UpdateAuthConfig(ctx, remote.ProjectId, remote.Auth, filter...); err != nil { - return err - } - if err := u.UpdateStorageConfig(ctx, remote.ProjectId, remote.Storage, filter...); err != nil { - return err - } - if err := u.UpdateExperimentalConfig(ctx, remote.ProjectId, remote.Experimental, filter...); err != nil { - return err - } - return nil -} - -func (u *ConfigUpdater) UpdateApiConfig(ctx context.Context, projectRef string, c api, filter ...func(string) bool) error { - apiConfig, err := u.client.V1GetPostgrestServiceConfigWithResponse(ctx, projectRef) - if err != nil { - return errors.Errorf("failed to read API config: %w", err) - } else if apiConfig.JSON200 == nil { - return errors.Errorf("unexpected status %d: %s", apiConfig.StatusCode(), string(apiConfig.Body)) - } - apiDiff, err := c.DiffWithRemote(*apiConfig.JSON200) - if err != nil { - return err - } else if len(apiDiff) == 0 { - fmt.Fprintln(os.Stderr, "Remote API config is up to date.") - return nil - } - fmt.Fprintln(os.Stderr, "Updating API service with config:", string(apiDiff)) - for _, keep := range filter { - if !keep("api") { - return nil - } - } - if resp, err := u.client.V1UpdatePostgrestServiceConfigWithResponse(ctx, projectRef, c.ToUpdatePostgrestConfigBody()); err != nil { - return errors.Errorf("failed to update API config: %w", err) - } else if resp.JSON200 == nil { - return errors.Errorf("unexpected status %d: %s", resp.StatusCode(), string(resp.Body)) - } - return nil -} - -func (u *ConfigUpdater) UpdateDbSettingsConfig(ctx context.Context, projectRef string, s settings, filter ...func(string) bool) error { - dbConfig, err := u.client.V1GetPostgresConfigWithResponse(ctx, projectRef) - if err != nil { - return errors.Errorf("failed to read DB config: %w", err) - } else if dbConfig.JSON200 == nil { - return errors.Errorf("unexpected status %d: %s", dbConfig.StatusCode(), string(dbConfig.Body)) - } - dbDiff, err := s.DiffWithRemote(*dbConfig.JSON200) - if err != nil { - return err - } else if len(dbDiff) == 0 { - fmt.Fprintln(os.Stderr, "Remote DB config is up to date.") - return nil - } - fmt.Fprintln(os.Stderr, "Updating DB service with config:", string(dbDiff)) - for _, keep := range filter { - if !keep("db") { - return nil - } - } - updateBody := s.ToUpdatePostgresConfigBody() - if resp, err := u.client.V1UpdatePostgresConfigWithResponse(ctx, projectRef, updateBody); err != nil { - return errors.Errorf("failed to update DB config: %w", err) - } else if resp.JSON200 == nil { - return errors.Errorf("unexpected status %d: %s", resp.StatusCode(), string(resp.Body)) - } - return nil -} - -func (u *ConfigUpdater) UpdateDbConfig(ctx context.Context, projectRef string, c db, filter ...func(string) bool) error { - if err := u.UpdateDbSettingsConfig(ctx, projectRef, c.Settings, filter...); err != nil { - return err - } - if err := u.UpdateDbNetworkRestrictionsConfig(ctx, projectRef, c.NetworkRestrictions, filter...); err != nil { - return err - } - if c.SslEnforcement != nil { - return u.UpdateSslEnforcement(ctx, projectRef, *c.SslEnforcement, filter...) - } - return nil -} - -func (u *ConfigUpdater) UpdateDbNetworkRestrictionsConfig(ctx context.Context, projectRef string, n networkRestrictions, filter ...func(string) bool) error { - if !n.Enabled { - return nil - } - networkRestrictionsConfig, err := u.client.V1GetNetworkRestrictionsWithResponse(ctx, projectRef) - if err != nil { - return errors.Errorf("failed to read network restrictions config: %w", err) - } else if networkRestrictionsConfig.JSON200 == nil { - return errors.Errorf("unexpected status %d: %s", networkRestrictionsConfig.StatusCode(), string(networkRestrictionsConfig.Body)) - } - networkRestrictionsDiff, err := n.DiffWithRemote(*networkRestrictionsConfig.JSON200) - if err != nil { - return err - } else if len(networkRestrictionsDiff) == 0 { - fmt.Fprintln(os.Stderr, "Remote DB Network restrictions config is up to date.") - return nil - } - fmt.Fprintln(os.Stderr, "Updating network restrictions with config:", string(networkRestrictionsDiff)) - for _, keep := range filter { - if !keep("db") { - return nil - } - } - updateBody := n.ToUpdateNetworkRestrictionsBody() - if resp, err := u.client.V1UpdateNetworkRestrictionsWithResponse(ctx, projectRef, updateBody); err != nil { - return errors.Errorf("failed to update network restrictions config: %w", err) - } else if resp.JSON201 == nil { - return errors.Errorf("unexpected status %d: %s", resp.StatusCode(), string(resp.Body)) - } - return nil -} - -func (u *ConfigUpdater) UpdateSslEnforcement(ctx context.Context, projectRef string, s sslEnforcement, filter ...func(string) bool) error { - sslEnforcementConfig, err := u.client.V1GetSslEnforcementConfigWithResponse(ctx, projectRef) - if err != nil { - return errors.Errorf("failed to read SSL enforcement config: %w", err) - } else if sslEnforcementConfig.JSON200 == nil { - return errors.Errorf("unexpected status %d: %s", sslEnforcementConfig.StatusCode(), string(sslEnforcementConfig.Body)) - } - sslEnforcementDiff, err := s.DiffWithRemote(*sslEnforcementConfig.JSON200) - if err != nil { - return err - } else if len(sslEnforcementDiff) == 0 { - fmt.Fprintln(os.Stderr, "Remote DB SSL enforcement config is up to date.") - return nil - } - fmt.Fprintln(os.Stderr, "Updating SSL enforcement with config:", string(sslEnforcementDiff)) - for _, keep := range filter { - if !keep("db") { - return nil - } - } - updateBody := s.ToUpdateSslEnforcementBody() - if resp, err := u.client.V1UpdateSslEnforcementConfigWithResponse(ctx, projectRef, updateBody); err != nil { - return errors.Errorf("failed to update SSL enforcement config: %w", err) - } else if resp.JSON200 == nil { - return errors.Errorf("unexpected status %d: %s", resp.StatusCode(), string(resp.Body)) - } - return nil -} - -func (u *ConfigUpdater) UpdateAuthConfig(ctx context.Context, projectRef string, c auth, filter ...func(string) bool) error { - if !c.Enabled { - return nil - } - authConfig, err := u.client.V1GetAuthServiceConfigWithResponse(ctx, projectRef) - if err != nil { - return errors.Errorf("failed to read Auth config: %w", err) - } else if authConfig.JSON200 == nil { - return errors.Errorf("unexpected status %d: %s", authConfig.StatusCode(), string(authConfig.Body)) - } - authDiff, err := c.DiffWithRemote(*authConfig.JSON200, filter...) - if err != nil { - return err - } else if len(authDiff) == 0 { - fmt.Fprintln(os.Stderr, "Remote Auth config is up to date.") - return nil - } - fmt.Fprintln(os.Stderr, "Updating Auth service with config:", string(authDiff)) - for _, keep := range filter { - if !keep("auth") { - return nil - } - } - if resp, err := u.client.V1UpdateAuthServiceConfigWithResponse(ctx, projectRef, c.ToUpdateAuthConfigBody()); err != nil { - return errors.Errorf("failed to update Auth config: %w", err) - } else if status := resp.StatusCode(); status < 200 || status >= 300 { - return errors.Errorf("unexpected status %d: %s", status, string(resp.Body)) - } - return nil -} - -func (u *ConfigUpdater) UpdateSigningKeys(ctx context.Context, projectRef string, signingKeys []JWK, filter ...func(string) bool) error { - if len(signingKeys) == 0 { - return nil - } - resp, err := u.client.V1GetProjectSigningKeysWithResponse(ctx, projectRef) - if err != nil { - return errors.Errorf("failed to fetch signing keys: %w", err) - } else if resp.JSON200 == nil { - return errors.Errorf("unexpected status %d: %s", resp.StatusCode(), string(resp.Body)) - } - exists := map[string]struct{}{} - for _, k := range resp.JSON200.Keys { - if k.PublicJwk != nil { - exists[k.Id.String()] = struct{}{} - } - } - var toInsert []JWK - for _, k := range signingKeys { - if _, ok := exists[k.KeyID]; !ok { - toInsert = append(toInsert, k) - } - } - if len(toInsert) == 0 { - fmt.Fprintln(os.Stderr, "Remote JWT signing keys are up to date.") - return nil - } - fmt.Fprintln(os.Stderr, "JWT signing keys to insert:") - for _, k := range toInsert { - fmt.Fprintln(os.Stderr, " -", k.KeyID) - } - for _, keep := range filter { - if !keep("signing keys") { - return nil - } - } - for _, k := range toInsert { - body := v1API.CreateSigningKeyBody{ - Algorithm: v1API.CreateSigningKeyBodyAlgorithm(k.Algorithm), - PrivateJwk: &v1API.CreateSigningKeyBody_PrivateJwk{}, - } - switch k.Algorithm { - case AlgRS256: - body.PrivateJwk.FromCreateSigningKeyBodyPrivateJwk0(v1API.CreateSigningKeyBodyPrivateJwk0{ - D: k.PrivateExponent, - Dp: k.FirstFactorCRTExponent, - Dq: k.SecondFactorCRTExponent, - E: v1API.CreateSigningKeyBodyPrivateJwk0E(k.Exponent), - Kty: v1API.CreateSigningKeyBodyPrivateJwk0Kty(k.KeyType), - N: k.Modulus, - P: k.FirstPrimeFactor, - Q: k.SecondPrimeFactor, - Qi: k.FirstCRTCoefficient, - }) - case AlgES256: - body.PrivateJwk.FromCreateSigningKeyBodyPrivateJwk1(v1API.CreateSigningKeyBodyPrivateJwk1{ - Crv: v1API.CreateSigningKeyBodyPrivateJwk1Crv(k.Curve), - D: k.PrivateExponent, - Kty: v1API.CreateSigningKeyBodyPrivateJwk1Kty(k.KeyType), - X: k.X, - Y: k.Y, - }) - } - if resp, err := u.client.V1CreateProjectSigningKeyWithResponse(ctx, projectRef, body); err != nil { - return errors.Errorf("failed to add signing key: %w", err) - } else if status := resp.StatusCode(); status < 200 || status >= 300 { - return errors.Errorf("unexpected status %d: %s", status, string(resp.Body)) - } - } - return nil -} - -func (u *ConfigUpdater) UpdateStorageConfig(ctx context.Context, projectRef string, c storage, filter ...func(string) bool) error { - if !c.Enabled { - return nil - } - storageConfig, err := u.client.V1GetStorageConfigWithResponse(ctx, projectRef) - if err != nil { - return errors.Errorf("failed to read Storage config: %w", err) - } else if storageConfig.JSON200 == nil { - return errors.Errorf("unexpected status %d: %s", storageConfig.StatusCode(), string(storageConfig.Body)) - } - storageDiff, err := c.DiffWithRemote(*storageConfig.JSON200) - if err != nil { - return err - } else if len(storageDiff) == 0 { - fmt.Fprintln(os.Stderr, "Remote Storage config is up to date.") - return nil - } - fmt.Fprintln(os.Stderr, "Updating Storage service with config:", string(storageDiff)) - for _, keep := range filter { - if !keep("storage") { - return nil - } - } - if resp, err := u.client.V1UpdateStorageConfigWithResponse(ctx, projectRef, c.ToUpdateStorageConfigBody()); err != nil { - return errors.Errorf("failed to update Storage config: %w", err) - } else if status := resp.StatusCode(); status < 200 || status >= 300 { - return errors.Errorf("unexpected status %d: %s", status, string(resp.Body)) - } - return nil -} - -func (u *ConfigUpdater) UpdateExperimentalConfig(ctx context.Context, projectRef string, exp experimental, filter ...func(string) bool) error { - if exp.Webhooks != nil && exp.Webhooks.Enabled { - fmt.Fprintln(os.Stderr, "Enabling webhooks for project:", projectRef) - for _, keep := range filter { - if !keep("webhooks") { - return nil - } - } - if resp, err := u.client.V1EnableDatabaseWebhookWithResponse(ctx, projectRef); err != nil { - return errors.Errorf("failed to enable webhooks: %w", err) - } else if status := resp.StatusCode(); status < 200 || status >= 300 { - return errors.Errorf("unexpected enable webhook status %d: %s", status, string(resp.Body)) - } - } - return nil -} diff --git a/apps/cli-go/pkg/config/updater_test.go b/apps/cli-go/pkg/config/updater_test.go deleted file mode 100644 index 0f00aba3b8..0000000000 --- a/apps/cli-go/pkg/config/updater_test.go +++ /dev/null @@ -1,397 +0,0 @@ -package config - -import ( - "context" - "net/http" - "testing" - - "github.com/h2non/gock" - "github.com/oapi-codegen/nullable" - openapi_types "github.com/oapi-codegen/runtime/types" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - v1API "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/cast" -) - -func TestUpdateApi(t *testing.T) { - server := "http://localhost" - client, err := v1API.NewClientWithResponses(server) - require.NoError(t, err) - - t.Run("updates remote config", func(t *testing.T) { - updater := NewConfigUpdater(*client) - // Setup mock server - defer gock.Off() - gock.New(server). - Get("/v1/projects/test-project/postgrest"). - Reply(http.StatusOK). - JSON(v1API.PostgrestConfigWithJWTSecretResponseOutput{}) - gock.New(server). - Patch("/v1/projects/test-project/postgrest"). - Reply(http.StatusOK). - JSON(v1API.PostgrestConfigWithJWTSecretResponseOutput{ - DbSchema: "public,graphql_public", - DbExtraSearchPath: "public,extensions", - MaxRows: 1000, - }) - // Run test - err := updater.UpdateApiConfig(context.Background(), "test-project", api{ - Enabled: true, - Schemas: []string{"public", "graphql_public"}, - ExtraSearchPath: []string{"public", "extensions"}, - MaxRows: 1000, - }) - // Check result - assert.NoError(t, err) - assert.True(t, gock.IsDone()) - }) - - t.Run("skips update if no diff", func(t *testing.T) { - updater := NewConfigUpdater(*client) - // Setup mock server - defer gock.Off() - gock.New(server). - Get("/v1/projects/test-project/postgrest"). - Reply(http.StatusOK). - JSON(v1API.PostgrestConfigWithJWTSecretResponseOutput{ - DbSchema: "", - DbExtraSearchPath: "public,extensions", - MaxRows: 1000, - }) - // Run test - err := updater.UpdateApiConfig(context.Background(), "test-project", api{}) - // Check result - assert.NoError(t, err) - assert.True(t, gock.IsDone()) - }) -} - -func TestUpdateDbConfig(t *testing.T) { - server := "http://localhost" - client, err := v1API.NewClientWithResponses(server) - require.NoError(t, err) - - t.Run("updates remote DB config", func(t *testing.T) { - updater := NewConfigUpdater(*client) - // Setup mock server - defer gock.Off() - gock.New(server). - Get("/v1/projects/test-project/config/database"). - Reply(http.StatusOK). - JSON(v1API.PostgresConfigResponseOutput{}) - gock.New(server). - Put("/v1/projects/test-project/config/database"). - Reply(http.StatusOK). - JSON(v1API.PostgresConfigResponseOutput{ - MaxConnections: cast.Ptr(cast.UintToInt(100)), - }) - // Run test - err := updater.UpdateDbConfig(context.Background(), "test-project", db{ - Settings: settings{ - MaxConnections: cast.Ptr(cast.IntToUint(100)), - }, - }) - // Check result - assert.NoError(t, err) - assert.True(t, gock.IsDone()) - }) - - t.Run("skips update if no diff in DB config", func(t *testing.T) { - updater := NewConfigUpdater(*client) - // Setup mock server - defer gock.Off() - gock.New(server). - Get("/v1/projects/test-project/config/database"). - Reply(http.StatusOK). - JSON(v1API.PostgresConfigResponseOutput{ - MaxConnections: cast.Ptr(cast.UintToInt(100)), - }) - // Run test - err := updater.UpdateDbConfig(context.Background(), "test-project", db{ - Settings: settings{ - MaxConnections: cast.Ptr(cast.IntToUint(100)), - }, - }) - // Check result - assert.NoError(t, err) - assert.True(t, gock.IsDone()) - }) -} - -func TestUpdateDbNetworkRestrictionsConfig(t *testing.T) { - server := "http://localhost" - client, err := v1API.NewClientWithResponses(server) - require.NoError(t, err) - - t.Run("skips update if disabled locally", func(t *testing.T) { - updater := NewConfigUpdater(*client) - // Run test - err := updater.UpdateDbNetworkRestrictionsConfig(context.Background(), "test-project", networkRestrictions{}) - // Check result - assert.NoError(t, err) - assert.False(t, gock.HasUnmatchedRequest()) - }) - - t.Run("returns error on 400 when enabled locally", func(t *testing.T) { - updater := NewConfigUpdater(*client) - // Setup mock server - defer gock.Off() - gock.New(server). - Get("/v1/projects/test-project/network-restrictions"). - Reply(http.StatusBadRequest). - JSON(map[string]any{ - "message": "project not allowed to set up network restrictions", - }) - // Run test - err := updater.UpdateDbNetworkRestrictionsConfig(context.Background(), "test-project", networkRestrictions{ - Enabled: true, - }) - // Check result - assert.Error(t, err) - assert.Contains(t, err.Error(), "unexpected status 400") - assert.True(t, gock.IsDone()) - }) -} - -func TestUpdateExperimentalConfig(t *testing.T) { - server := "http://localhost" - client, err := v1API.NewClientWithResponses(server) - require.NoError(t, err) - - t.Run("enables webhooks", func(t *testing.T) { - updater := NewConfigUpdater(*client) - // Setup mock server - defer gock.Off() - gock.New(server). - Post("/v1/projects/test-project/database/webhooks/enable"). - Reply(http.StatusOK). - JSON(map[string]any{}) - // Run test - err := updater.UpdateExperimentalConfig(context.Background(), "test-project", experimental{ - Webhooks: &webhooks{ - Enabled: true, - }, - }) - // Check result - assert.NoError(t, err) - assert.True(t, gock.IsDone()) - }) - - t.Run("skips update if webhooks not enabled", func(t *testing.T) { - updater := NewConfigUpdater(*client) - // Run test - err := updater.UpdateExperimentalConfig(context.Background(), "test-project", experimental{ - Webhooks: &webhooks{ - Enabled: false, - }, - }) - // Check result - assert.NoError(t, err) - assert.True(t, gock.IsDone()) - }) -} - -func TestUpdateAuthConfig(t *testing.T) { - server := "http://localhost" - client, err := v1API.NewClientWithResponses(server) - require.NoError(t, err) - - t.Run("updates remote Auth config", func(t *testing.T) { - updater := NewConfigUpdater(*client) - // Setup mock server - defer gock.Off() - gock.New(server). - Get("/v1/projects/test-project/config/auth"). - Reply(http.StatusOK). - JSON(v1API.AuthConfigResponseOutput{ - SiteUrl: nullable.NewNullableWithValue("http://localhost:3000"), - SmtpAdminEmail: nullable.NewNullableWithValue(openapi_types.Email("abc@example.com")), - }) - gock.New(server). - Patch("/v1/projects/test-project/config/auth"). - Reply(http.StatusOK) - // Run test - err := updater.UpdateAuthConfig(context.Background(), "test-project", auth{Enabled: true}) - // Check result - assert.NoError(t, err) - assert.True(t, gock.IsDone()) - }) - - t.Run("skips update if no diff in Auth config", func(t *testing.T) { - updater := NewConfigUpdater(*client) - // Setup mock server - defer gock.Off() - gock.New(server). - Get("/v1/projects/test-project/config/auth"). - Reply(http.StatusOK). - JSON(v1API.AuthConfigResponseOutput{ - SmtpAdminEmail: nullable.NewNullableWithValue(openapi_types.Email("abc@example.com")), - }) - // Run test - err := updater.UpdateAuthConfig(context.Background(), "test-project", auth{ - Enabled: true, - EnableSignup: true, - AdditionalRedirectUrls: []string{}, - Email: email{EnableConfirmations: true}, - Sms: sms{TestOTP: map[string]string{}}, - }) - // Check result - assert.NoError(t, err) - assert.True(t, gock.IsDone()) - }) - - t.Run("skips update if disabled locally", func(t *testing.T) { - updater := NewConfigUpdater(*client) - // Run test - err := updater.UpdateAuthConfig(context.Background(), "test-project", auth{}) - // Check result - assert.NoError(t, err) - }) -} - -func TestUpdateStorageConfig(t *testing.T) { - server := "http://localhost" - client, err := v1API.NewClientWithResponses(server) - require.NoError(t, err) - - t.Run("updates remote Storage config", func(t *testing.T) { - updater := NewConfigUpdater(*client) - // Setup mock server - defer gock.Off() - mockStorage := v1API.StorageConfigResponseOutput{ - FileSizeLimit: 100, - } - mockStorage.Features.ImageTransformation.Enabled = true - mockStorage.Features.S3Protocol.Enabled = true - gock.New(server). - Get("/v1/projects/test-project/config/storage"). - Reply(http.StatusOK). - JSON(mockStorage) - gock.New(server). - Patch("/v1/projects/test-project/config/storage"). - Reply(http.StatusOK) - // Run test - err := updater.UpdateStorageConfig(context.Background(), "test-project", storage{Enabled: true}) - // Check result - assert.NoError(t, err) - assert.True(t, gock.IsDone()) - }) - - t.Run("skips update if no diff in Storage config", func(t *testing.T) { - updater := NewConfigUpdater(*client) - // Setup mock server - defer gock.Off() - gock.New(server). - Get("/v1/projects/test-project/config/storage"). - Reply(http.StatusOK). - JSON(v1API.StorageConfigResponseOutput{}) - // Run test - err := updater.UpdateStorageConfig(context.Background(), "test-project", storage{Enabled: true}) - // Check result - assert.NoError(t, err) - assert.True(t, gock.IsDone()) - }) - - t.Run("skips update if disabled locally", func(t *testing.T) { - updater := NewConfigUpdater(*client) - // Run test - err := updater.UpdateStorageConfig(context.Background(), "test-project", storage{}) - // Check result - assert.NoError(t, err) - }) -} - -func TestUpdateRemoteConfig(t *testing.T) { - server := "http://localhost" - client, err := v1API.NewClientWithResponses(server) - require.NoError(t, err) - - t.Run("updates all configs", func(t *testing.T) { - updater := NewConfigUpdater(*client) - // Setup mock server - defer gock.Off() - // API config - gock.New(server). - Get("/v1/projects/test-project/postgrest"). - Reply(http.StatusOK). - JSON(v1API.PostgrestConfigWithJWTSecretResponseOutput{}) - gock.New(server). - Patch("/v1/projects/test-project/postgrest"). - Reply(http.StatusOK). - JSON(v1API.PostgrestConfigWithJWTSecretResponseOutput{ - DbSchema: "public", - MaxRows: 1000, - }) - // DB config - gock.New(server). - Get("/v1/projects/test-project/config/database"). - Reply(http.StatusOK). - JSON(v1API.PostgresConfigResponseOutput{}) - gock.New(server). - Put("/v1/projects/test-project/config/database"). - Reply(http.StatusOK). - JSON(v1API.PostgresConfigResponseOutput{ - MaxConnections: cast.Ptr(cast.UintToInt(100)), - }) - // Auth config - gock.New(server). - Get("/v1/projects/test-project/config/auth"). - Reply(http.StatusOK). - JSON(v1API.AuthConfigResponseOutput{ - SmtpAdminEmail: nullable.NewNullableWithValue(openapi_types.Email("abc@example.com")), - }) - gock.New(server). - Patch("/v1/projects/test-project/config/auth"). - Reply(http.StatusOK) - // Storage config - gock.New(server). - Get("/v1/projects/test-project/config/storage"). - Reply(http.StatusOK). - JSON(v1API.StorageConfigResponseOutput{}) - gock.New(server). - Patch("/v1/projects/test-project/config/storage"). - Reply(http.StatusOK) - // Experimental config - gock.New(server). - Post("/v1/projects/test-project/database/webhooks/enable"). - Reply(http.StatusOK). - JSON(map[string]any{}) - // Run test - err := updater.UpdateRemoteConfig(context.Background(), baseConfig{ - ProjectId: "test-project", - Api: api{ - Enabled: true, - Schemas: []string{"public", "private"}, - MaxRows: 1000, - }, - Db: db{ - Settings: settings{ - MaxConnections: cast.Ptr(cast.IntToUint(100)), - }, - }, - Auth: auth{ - Enabled: true, - SiteUrl: "http://localhost:3000", - }, - Storage: storage{ - Enabled: true, - FileSizeLimit: 100, - ImageTransformation: &imageTransformation{ - Enabled: true, - }, - S3Protocol: &s3Protocol{ - Enabled: true, - }, - }, - Experimental: experimental{ - Webhooks: &webhooks{ - Enabled: true, - }, - }, - }) - // Check result - assert.NoError(t, err) - assert.True(t, gock.IsDone()) - }) -} diff --git a/apps/cli-go/pkg/config/utils.go b/apps/cli-go/pkg/config/utils.go deleted file mode 100644 index bd2b9512b6..0000000000 --- a/apps/cli-go/pkg/config/utils.go +++ /dev/null @@ -1,150 +0,0 @@ -package config - -import ( - "crypto/hmac" - "crypto/sha256" - "encoding/hex" - "fmt" - "io/fs" - "os" - "path/filepath" - "strconv" - "strings" -) - -// StackStateEnv is set by the TypeScript CLI when the project keeps its linked state outside -// supabase/.temp, so the files under it are neither read nor written. -const StackStateEnv = "SUPABASE_INTERNAL_STACK_STATE" - -// StackPoolerUrlEnv carries the linked pooler connection string in stack mode, where -// supabase/.temp/pooler-url is not read. -const StackPoolerUrlEnv = "SUPABASE_INTERNAL_POOLER_URL" - -// UsesStackState reports whether supabase/.temp is off limits for this invocation. -func UsesStackState() bool { - enabled, _ := strconv.ParseBool(os.Getenv(StackStateEnv)) - return enabled -} - -// readTempFile reads a file under supabase/.temp, which reads as absent in stack mode. -func readTempFile(fsys fs.FS, path string) ([]byte, error) { - if UsesStackState() { - return nil, fs.ErrNotExist - } - return fs.ReadFile(fsys, path) -} - -// readLinkedPoolerUrl returns the linked pooler connection string from supabase/.temp, or from the -// environment in stack mode. -func readLinkedPoolerUrl(fsys fs.FS, path string) ([]byte, error) { - if UsesStackState() { - return []byte(os.Getenv(StackPoolerUrlEnv)), nil - } - return fs.ReadFile(fsys, path) -} - -type pathBuilder struct { - SupabaseDirPath string - ConfigPath string - GitIgnorePath string - TempDir string - ImportMapsDir string - ProjectRefPath string - PoolerUrlPath string - PostgresVersionPath string - GotrueVersionPath string - RestVersionPath string - StorageVersionPath string - StorageMigrationPath string - StudioVersionPath string - PgmetaVersionPath string - PoolerVersionPath string - RealtimeVersionPath string - EdgeRuntimeVersionPath string - LogflareVersionPath string - CliVersionPath string - CurrBranchPath string - SchemasDir string - MigrationsDir string - FunctionsDir string - FallbackImportMapPath string - FallbackEnvFilePath string - DbTestsDir string - CustomRolesPath string -} - -func NewPathBuilder(configPath string) pathBuilder { - if filepath.Base(configPath) == "." { - configPath = filepath.Join("supabase", "config.toml") - } - // TODO: make base path configurable from toml - base := filepath.Dir(configPath) - return pathBuilder{ - SupabaseDirPath: base, - ConfigPath: configPath, - GitIgnorePath: filepath.Join(base, ".gitignore"), - TempDir: filepath.Join(base, ".temp"), - ImportMapsDir: filepath.Join(base, ".temp", "import_maps"), - ProjectRefPath: filepath.Join(base, ".temp", "project-ref"), - PoolerUrlPath: filepath.Join(base, ".temp", "pooler-url"), - PostgresVersionPath: filepath.Join(base, ".temp", "postgres-version"), - GotrueVersionPath: filepath.Join(base, ".temp", "gotrue-version"), - RestVersionPath: filepath.Join(base, ".temp", "rest-version"), - StorageVersionPath: filepath.Join(base, ".temp", "storage-version"), - StorageMigrationPath: filepath.Join(base, ".temp", "storage-migration"), - EdgeRuntimeVersionPath: filepath.Join(base, ".temp", "edge-runtime-version"), - StudioVersionPath: filepath.Join(base, ".temp", "studio-version"), - PgmetaVersionPath: filepath.Join(base, ".temp", "pgmeta-version"), - PoolerVersionPath: filepath.Join(base, ".temp", "pooler-version"), - RealtimeVersionPath: filepath.Join(base, ".temp", "realtime-version"), - LogflareVersionPath: filepath.Join(base, ".temp", "logflare-version"), - CliVersionPath: filepath.Join(base, ".temp", "cli-latest"), - CurrBranchPath: filepath.Join(base, ".branches", "_current_branch"), - SchemasDir: filepath.Join(base, "schemas"), - MigrationsDir: filepath.Join(base, "migrations"), - FunctionsDir: filepath.Join(base, "functions"), - FallbackImportMapPath: filepath.Join(base, "functions", "import_map.json"), - FallbackEnvFilePath: filepath.Join(base, "functions", ".env"), - DbTestsDir: filepath.Join(base, "tests"), - CustomRolesPath: filepath.Join(base, "roles.sql"), - } -} - -func replaceImageTag(image string, tag string) string { - index := strings.IndexByte(image, ':') - return image[:index+1] + strings.TrimSpace(tag) -} - -func strToArr(v string) []string { - // Avoid returning [""] if v is empty - if len(v) == 0 { - return []string{} - } - return strings.Split(v, ",") -} - -func mapToEnv(input map[string]string) string { - var result []string - for k, v := range input { - kv := fmt.Sprintf("%s=%s", k, v) - result = append(result, kv) - } - return strings.Join(result, ",") -} - -func envToMap(input string) map[string]string { - env := strToArr(input) - result := make(map[string]string, len(env)) - for _, kv := range env { - if parts := strings.Split(kv, "="); len(parts) > 1 { - result[parts[0]] = parts[1] - } - } - return result -} - -func sha256Hmac(key, value string) string { - h := hmac.New(sha256.New, []byte(key)) - h.Write([]byte(value)) - return hex.EncodeToString(h.Sum(nil)) -} diff --git a/apps/cli-go/pkg/diff/diff.go b/apps/cli-go/pkg/diff/diff.go deleted file mode 100644 index e3a8e2d981..0000000000 --- a/apps/cli-go/pkg/diff/diff.go +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package diff - -import ( - "bytes" - "fmt" - "sort" - "strings" -) - -// A pair is a pair of values tracked for both the x and y side of a diff. -// It is typically a pair of line indexes. -type pair struct{ x, y int } - -// Diff returns an anchored diff of the two texts old and new -// in the “unified diff” format. If old and new are identical, -// Diff returns a nil slice (no output). -// -// Unix diff implementations typically look for a diff with -// the smallest number of lines inserted and removed, -// which can in the worst case take time quadratic in the -// number of lines in the texts. As a result, many implementations -// either can be made to run for a long time or cut off the search -// after a predetermined amount of work. -// -// In contrast, this implementation looks for a diff with the -// smallest number of “unique” lines inserted and removed, -// where unique means a line that appears just once in both old and new. -// We call this an “anchored diff” because the unique lines anchor -// the chosen matching regions. An anchored diff is usually clearer -// than a standard diff, because the algorithm does not try to -// reuse unrelated blank lines or closing braces. -// The algorithm also guarantees to run in O(n log n) time -// instead of the standard O(n²) time. -// -// Some systems call this approach a “patience diff,” named for -// the “patience sorting” algorithm, itself named for a solitaire card game. -// We avoid that name for two reasons. First, the name has been used -// for a few different variants of the algorithm, so it is imprecise. -// Second, the name is frequently interpreted as meaning that you have -// to wait longer (to be patient) for the diff, meaning that it is a slower algorithm, -// when in fact the algorithm is faster than the standard one. -func Diff(oldName string, old []byte, newName string, new []byte) []byte { - if bytes.Equal(old, new) { - return nil - } - x := lines(old) - y := lines(new) - - // Print diff header. - var out bytes.Buffer - fmt.Fprintf(&out, "diff %s %s\n", oldName, newName) - fmt.Fprintf(&out, "--- %s\n", oldName) - fmt.Fprintf(&out, "+++ %s\n", newName) - - // Loop over matches to consider, - // expanding each match to include surrounding lines, - // and then printing diff chunks. - // To avoid setup/teardown cases outside the loop, - // tgs returns a leading {0,0} and trailing {len(x), len(y)} pair - // in the sequence of matches. - var ( - done pair // printed up to x[:done.x] and y[:done.y] - chunk pair // start lines of current chunk - count pair // number of lines from each side in current chunk - ctext []string // lines for current chunk - ) - for _, m := range tgs(x, y) { - if m.x < done.x { - // Already handled scanning forward from earlier match. - continue - } - - // Expand matching lines as far as possible, - // establishing that x[start.x:end.x] == y[start.y:end.y]. - // Note that on the first (or last) iteration we may (or definitely do) - // have an empty match: start.x==end.x and start.y==end.y. - start := m - for start.x > done.x && start.y > done.y && x[start.x-1] == y[start.y-1] { - start.x-- - start.y-- - } - end := m - for end.x < len(x) && end.y < len(y) && x[end.x] == y[end.y] { - end.x++ - end.y++ - } - - // Emit the mismatched lines before start into this chunk. - // (No effect on first sentinel iteration, when start = {0,0}.) - for _, s := range x[done.x:start.x] { - ctext = append(ctext, "-"+s) - count.x++ - } - for _, s := range y[done.y:start.y] { - ctext = append(ctext, "+"+s) - count.y++ - } - - // If we're not at EOF and have too few common lines, - // the chunk includes all the common lines and continues. - const C = 3 // number of context lines - if (end.x < len(x) || end.y < len(y)) && - (end.x-start.x < C || (len(ctext) > 0 && end.x-start.x < 2*C)) { - for _, s := range x[start.x:end.x] { - ctext = append(ctext, " "+s) - count.x++ - count.y++ - } - done = end - continue - } - - // End chunk with common lines for context. - if len(ctext) > 0 { - n := min(end.x-start.x, C) - for _, s := range x[start.x : start.x+n] { - ctext = append(ctext, " "+s) - count.x++ - count.y++ - } - done = pair{start.x + n, start.y + n} - - // Format and emit chunk. - // Convert line numbers to 1-indexed. - // Special case: empty file shows up as 0,0 not 1,0. - if count.x > 0 { - chunk.x++ - } - if count.y > 0 { - chunk.y++ - } - fmt.Fprintf(&out, "@@ -%d,%d +%d,%d @@\n", chunk.x, count.x, chunk.y, count.y) - for _, s := range ctext { - out.WriteString(s) - } - count.x = 0 - count.y = 0 - ctext = ctext[:0] - } - - // If we reached EOF, we're done. - if end.x >= len(x) && end.y >= len(y) { - break - } - - // Otherwise start a new chunk. - chunk = pair{end.x - C, end.y - C} - for _, s := range x[chunk.x:end.x] { - ctext = append(ctext, " "+s) - count.x++ - count.y++ - } - done = end - } - - return out.Bytes() -} - -// lines returns the lines in the file x, including newlines. -// If the file does not end in a newline, one is supplied -// along with a warning about the missing newline. -func lines(x []byte) []string { - l := strings.SplitAfter(string(x), "\n") - if l[len(l)-1] == "" { - l = l[:len(l)-1] - } else { - // Treat last line as having a message about the missing newline attached, - // using the same text as BSD/GNU diff (including the leading backslash). - l[len(l)-1] += "\n\\ No newline at end of file\n" - } - return l -} - -// tgs returns the pairs of indexes of the longest common subsequence -// of unique lines in x and y, where a unique line is one that appears -// once in x and once in y. -// -// The longest common subsequence algorithm is as described in -// Thomas G. Szymanski, “A Special Case of the Maximal Common -// Subsequence Problem,” Princeton TR #170 (January 1975), -// available at https://research.swtch.com/tgs170.pdf. -func tgs(x, y []string) []pair { - // Count the number of times each string appears in a and b. - // We only care about 0, 1, many, counted as 0, -1, -2 - // for the x side and 0, -4, -8 for the y side. - // Using negative numbers now lets us distinguish positive line numbers later. - m := make(map[string]int) - for _, s := range x { - if c := m[s]; c > -2 { - m[s] = c - 1 - } - } - for _, s := range y { - if c := m[s]; c > -8 { - m[s] = c - 4 - } - } - - // Now unique strings can be identified by m[s] = -1+-4. - // - // Gather the indexes of those strings in x and y, building: - // xi[i] = increasing indexes of unique strings in x. - // yi[i] = increasing indexes of unique strings in y. - // inv[i] = index j such that x[xi[i]] = y[yi[j]]. - var xi, yi, inv []int - for i, s := range y { - if m[s] == -1+-4 { - m[s] = len(yi) - yi = append(yi, i) - } - } - for i, s := range x { - if j, ok := m[s]; ok && j >= 0 { - xi = append(xi, i) - inv = append(inv, j) - } - } - - // Apply Algorithm A from Szymanski's paper. - // In those terms, A = J = inv and B = [0, n). - // We add sentinel pairs {0,0}, and {len(x),len(y)} - // to the returned sequence, to help the processing loop. - J := inv - n := len(xi) - T := make([]int, n) - L := make([]int, n) - for i := range T { - T[i] = n + 1 - } - for i := range n { - k := sort.Search(n, func(k int) bool { - return T[k] >= J[i] - }) - T[k] = J[i] - L[i] = k + 1 - } - k := 0 - for _, v := range L { - if k < v { - k = v - } - } - seq := make([]pair, 2+k) - seq[1+k] = pair{len(x), len(y)} // sentinel at end - lastj := n - for i := n - 1; i >= 0; i-- { - if L[i] == k && J[i] < lastj { - seq[k] = pair{xi[i], yi[J[i]]} - k-- - } - } - seq[0] = pair{0, 0} // sentinel at start - return seq -} diff --git a/apps/cli-go/pkg/fetcher/gateway.go b/apps/cli-go/pkg/fetcher/gateway.go deleted file mode 100644 index 694882066b..0000000000 --- a/apps/cli-go/pkg/fetcher/gateway.go +++ /dev/null @@ -1,34 +0,0 @@ -package fetcher - -import ( - "net/http" - "strings" -) - -// NewServiceGateway returns a Fetcher for Supabase service-gateway calls (Kong, -// Storage, Auth, etc.). It deliberately omits http.Client.Timeout so streaming -// operations such as storage uploads are not capped under load. Connection -// setup is still bounded by the default transport's dial and TLS-handshake -// timeouts, and per-call deadlines should flow through the request context. -func NewServiceGateway(server, token string, overrides ...FetcherOption) *Fetcher { - opts := append([]FetcherOption{ - WithHTTPClient(&http.Client{}), - withAuthToken(token), - WithExpectedStatus(http.StatusOK), - }, overrides...) - return NewFetcher(server, opts...) -} - -func withAuthToken(token string) FetcherOption { - if strings.HasPrefix(token, "sb_") { - header := func(req *http.Request) { - req.Header.Add("apikey", token) - } - return WithRequestEditor(header) - } - header := func(req *http.Request) { - req.Header.Add("apikey", token) - req.Header.Add("Authorization", "Bearer "+token) - } - return WithRequestEditor(header) -} diff --git a/apps/cli-go/pkg/fetcher/http.go b/apps/cli-go/pkg/fetcher/http.go deleted file mode 100644 index 3ea4ac2fe2..0000000000 --- a/apps/cli-go/pkg/fetcher/http.go +++ /dev/null @@ -1,152 +0,0 @@ -package fetcher - -import ( - "bytes" - "context" - "encoding/json" - "io" - "net/http" - "net/url" - "slices" - "strings" - - "github.com/go-errors/errors" -) - -type Fetcher struct { - server string - client *http.Client - editors []RequestEditor - status []int -} - -type FetcherOption func(*Fetcher) - -func NewFetcher(server string, opts ...FetcherOption) *Fetcher { - api := &Fetcher{ - server: server, - client: http.DefaultClient, - } - for _, apply := range opts { - apply(api) - } - return api -} - -func WithHTTPClient(client *http.Client) FetcherOption { - return func(s *Fetcher) { - s.client = client - } -} - -func WithExpectedStatus(statusCode ...int) FetcherOption { - return func(s *Fetcher) { - s.status = statusCode - } -} - -func WithBearerToken(token string) FetcherOption { - addHeader := func(req *http.Request) { - req.Header.Add("Authorization", "Bearer "+token) - } - return WithRequestEditor(addHeader) -} - -func WithUserAgent(agent string) FetcherOption { - addHeader := func(req *http.Request) { - req.Header.Add("User-Agent", agent) - } - return WithRequestEditor(addHeader) -} - -func WithRequestEditor(fn RequestEditor) FetcherOption { - return func(s *Fetcher) { - s.editors = append(s.editors, fn) - } -} - -type RequestEditor func(req *http.Request) - -func (s *Fetcher) Send(ctx context.Context, method, path string, reqBody any, reqEditors ...RequestEditor) (*http.Response, error) { - body, ok := reqBody.(io.Reader) - if !ok && reqBody != nil { - var buf bytes.Buffer - enc := json.NewEncoder(&buf) - if err := enc.Encode(reqBody); err != nil { - return nil, errors.Errorf("failed to encode request body: %w", err) - } - reqEditors = append(reqEditors, func(req *http.Request) { - req.Header.Set("Content-Type", "application/json") - }) - body = &buf - } - // Creates request - req, err := http.NewRequestWithContext(ctx, method, s.server+path, body) - if err != nil { - return nil, errors.Errorf("failed to initialise http request: %w", err) - } - for _, apply := range s.editors { - apply(req) - } - for _, apply := range reqEditors { - apply(req) - } - // Sends request - resp, err := s.client.Do(req) - if err != nil { - if hint := localGatewayHint(s.server, err); len(hint) > 0 { - return nil, errors.Errorf("failed to execute http request: %w\n\n%s", err, hint) - } - return nil, errors.Errorf("failed to execute http request: %w", err) - } - if slices.Contains(s.status, resp.StatusCode) { - return resp, nil - } - // Reject unexpected status codes as error - if len(s.status) > 0 || resp.StatusCode >= http.StatusBadRequest { - defer resp.Body.Close() - data, err := io.ReadAll(resp.Body) - if err != nil { - return resp, errors.Errorf("Error status %d: %w", resp.StatusCode, err) - } - return resp, errors.Errorf("Error status %d: %s", resp.StatusCode, data) - } - return resp, nil -} - -func localGatewayHint(server string, err error) string { - if err == nil { - return "" - } - parsed, parseErr := url.Parse(server) - if parseErr != nil { - return "" - } - host := parsed.Hostname() - if host != "127.0.0.1" && host != "localhost" && host != "::1" { - return "" - } - message := err.Error() - if !strings.Contains(message, "malformed HTTP response") && - !strings.Contains(message, "Client.Timeout exceeded while awaiting headers") && - !strings.Contains(message, "context deadline exceeded") { - return "" - } - port := parsed.Port() - if len(port) == 0 { - return "" - } - return "The local Supabase API gateway did not return a valid HTTP response. " + - "Another process may be listening on the configured API port " + port + ". " + - "Check the port with `lsof -nP -iTCP:" + port + " -sTCP:LISTEN`, then stop the conflicting process or set a different `api.port` in supabase/config.toml." -} - -func ParseJSON[T any](r io.ReadCloser) (T, error) { - defer r.Close() - var data T - dec := json.NewDecoder(r) - if err := dec.Decode(&data); err != nil { - return data, errors.Errorf("failed to parse response body: %w", err) - } - return data, nil -} diff --git a/apps/cli-go/pkg/fetcher/http_test.go b/apps/cli-go/pkg/fetcher/http_test.go deleted file mode 100644 index a24a0fa303..0000000000 --- a/apps/cli-go/pkg/fetcher/http_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package fetcher - -import ( - "context" - "net" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSendSuggestsApiPortConflictForMalformedLocalResponse(t *testing.T) { - listener, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - defer listener.Close() - - done := make(chan struct{}) - go func() { - defer close(done) - conn, err := listener.Accept() - if err != nil { - return - } - defer conn.Close() - _, _ = conn.Write([]byte(`{"type":"Tier1","version":"1.0"}`)) - }() - - api := NewFetcher("http://" + listener.Addr().String()) - _, err = api.Send(context.Background(), "GET", "/storage/v1/bucket", nil) - require.Error(t, err) - assert.Contains(t, err.Error(), "malformed HTTP response") - assert.Contains(t, err.Error(), "Another process may be listening on the configured API port") - assert.Contains(t, err.Error(), "lsof -nP -iTCP:") - assert.Contains(t, err.Error(), "api.port") - - <-done -} diff --git a/apps/cli-go/pkg/function/api.go b/apps/cli-go/pkg/function/api.go deleted file mode 100644 index df44432405..0000000000 --- a/apps/cli-go/pkg/function/api.go +++ /dev/null @@ -1,53 +0,0 @@ -package function - -import ( - "context" - "io" - - "github.com/supabase/cli/pkg/api" -) - -type EdgeRuntimeAPI struct { - project string - client api.ClientWithResponses - eszip EszipBundler - maxJobs uint -} - -type FunctionDeployMetadata struct { - EntrypointPath string `json:"entrypoint_path"` - ImportMapPath *string `json:"import_map_path,omitempty"` - Name *string `json:"name,omitempty"` - StaticPatterns *[]string `json:"static_patterns,omitempty"` - VerifyJwt *bool `json:"verify_jwt,omitempty"` - SHA256 string `json:"sha256,omitempty"` -} - -type EszipBundler interface { - Bundle(ctx context.Context, slug, entrypoint, importMap string, staticFiles []string, output io.Writer) (FunctionDeployMetadata, error) -} - -func NewEdgeRuntimeAPI(project string, client api.ClientWithResponses, opts ...withOption) EdgeRuntimeAPI { - result := EdgeRuntimeAPI{client: client, project: project} - for _, apply := range opts { - apply(&result) - } - if result.maxJobs == 0 { - result.maxJobs = 1 - } - return result -} - -type withOption func(*EdgeRuntimeAPI) - -func WithBundler(bundler EszipBundler) withOption { - return func(era *EdgeRuntimeAPI) { - era.eszip = bundler - } -} - -func WithMaxJobs(maxJobs uint) withOption { - return func(era *EdgeRuntimeAPI) { - era.maxJobs = maxJobs - } -} diff --git a/apps/cli-go/pkg/function/batch.go b/apps/cli-go/pkg/function/batch.go deleted file mode 100644 index 70d0f4298a..0000000000 --- a/apps/cli-go/pkg/function/batch.go +++ /dev/null @@ -1,170 +0,0 @@ -package function - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "fmt" - "io" - "net/http" - "os" - "strings" - "time" - - "github.com/cenkalti/backoff/v4" - "github.com/docker/go-units" - "github.com/go-errors/errors" - "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/cast" - "github.com/supabase/cli/pkg/config" -) - -const ( - eszipContentType = "application/vnd.denoland.eszip" - maxRetries = 3 -) - -func (s *EdgeRuntimeAPI) UpsertFunctions(ctx context.Context, functionConfig config.FunctionConfig, filter ...func(string) bool) error { - policy := backoff.WithContext(backoff.WithMaxRetries(backoff.NewExponentialBackOff(), maxRetries), ctx) - result, err := backoff.RetryWithData(func() ([]api.FunctionResponseOutput, error) { - resp, err := s.client.V1ListAllFunctionsWithResponse(ctx, s.project) - if err != nil { - return nil, errors.Errorf("failed to list functions: %w", err) - } else if resp.JSON200 == nil { - err = errors.Errorf("unexpected list functions status %d: %s", resp.StatusCode(), string(resp.Body)) - if resp.StatusCode() < http.StatusInternalServerError { - err = &backoff.PermanentError{Err: err} - } - return nil, err - } - return *resp.JSON200, nil - }, policy) - if err != nil { - return err - } - policy.Reset() - slugToIndex := make(map[string]int, len(result)) - for i, f := range result { - slugToIndex[f.Slug] = i - } - var toUpdate api.BulkUpdateFunctionBody -OUTER: - for slug, function := range functionConfig { - if !function.Enabled { - fmt.Fprintln(os.Stderr, "Skipping disabled Function:", slug) - continue - } - for _, keep := range filter { - if !keep(slug) { - continue OUTER - } - } - var body bytes.Buffer - meta, err := s.eszip.Bundle(ctx, slug, function.Entrypoint, function.ImportMap, function.StaticFiles, &body) - if errors.Is(err, ErrNoDeploy) { - fmt.Fprintln(os.Stderr, "Skipping undeployable Function:", slug) - continue - } else if err != nil { - return err - } - meta.VerifyJwt = function.VerifyJWT - bodyHash := sha256.Sum256(body.Bytes()) - meta.SHA256 = hex.EncodeToString(bodyHash[:]) - // Skip if function has not changed - if i, exists := slugToIndex[slug]; exists && i >= 0 && - result[i].EzbrSha256 != nil && *result[i].EzbrSha256 == meta.SHA256 && - (function.VerifyJWT == nil || result[i].VerifyJwt != nil && *result[i].VerifyJwt == *function.VerifyJWT) { - fmt.Fprintln(os.Stderr, "No change found in Function:", slug) - continue - } - // Update if function already exists - upsert := func() (api.BulkUpdateFunctionBody, error) { - if _, exists := slugToIndex[slug]; exists { - return s.updateFunction(ctx, slug, meta, bytes.NewReader(body.Bytes())) - } - return s.createFunction(ctx, slug, meta, bytes.NewReader(body.Bytes())) - } - functionSize := units.HumanSize(float64(body.Len())) - fmt.Fprintf(os.Stderr, "Deploying Function: %s (script size: %s)\n", slug, functionSize) - result, err := backoff.RetryNotifyWithData(upsert, policy, func(err error, d time.Duration) { - if strings.Contains(err.Error(), "Duplicated function slug") { - slugToIndex[slug] = -1 - } - }) - if err != nil { - return err - } - toUpdate = append(toUpdate, result...) - policy.Reset() - } - if len(toUpdate) > 1 { - if err := backoff.Retry(func() error { - if resp, err := s.client.V1BulkUpdateFunctionsWithResponse(ctx, s.project, toUpdate); err != nil { - return errors.Errorf("failed to bulk update: %w", err) - } else if resp.JSON200 == nil { - return errors.Errorf("unexpected bulk update status %d: %s", resp.StatusCode(), string(resp.Body)) - } - return nil - }, policy); err != nil { - return err - } - } - return nil -} - -func (s *EdgeRuntimeAPI) updateFunction(ctx context.Context, slug string, meta FunctionDeployMetadata, body io.Reader) (api.BulkUpdateFunctionBody, error) { - resp, err := s.client.V1UpdateAFunctionWithBodyWithResponse(ctx, s.project, slug, &api.V1UpdateAFunctionParams{ - VerifyJwt: cast.BoolToStringPtr(meta.VerifyJwt), - ImportMapPath: meta.ImportMapPath, - EntrypointPath: &meta.EntrypointPath, - EzbrSha256: &meta.SHA256, - }, eszipContentType, body) - if err != nil { - return api.BulkUpdateFunctionBody{}, errors.Errorf("failed to update function: %w", err) - } else if resp.JSON200 == nil { - return api.BulkUpdateFunctionBody{}, errors.Errorf("unexpected update function status %d: %s", resp.StatusCode(), string(resp.Body)) - } - return api.BulkUpdateFunctionBody{{ - Id: resp.JSON200.Id, - Name: resp.JSON200.Name, - Slug: resp.JSON200.Slug, - Version: resp.JSON200.Version, - EntrypointPath: resp.JSON200.EntrypointPath, - ImportMap: resp.JSON200.ImportMap, - ImportMapPath: resp.JSON200.ImportMapPath, - VerifyJwt: resp.JSON200.VerifyJwt, - Status: api.BulkUpdateFunctionBodyStatus(resp.JSON200.Status), - CreatedAt: &resp.JSON200.CreatedAt, - EzbrSha256: resp.JSON200.EzbrSha256, - }}, nil -} - -func (s *EdgeRuntimeAPI) createFunction(ctx context.Context, slug string, meta FunctionDeployMetadata, body io.Reader) (api.BulkUpdateFunctionBody, error) { - resp, err := s.client.V1CreateAFunctionWithBodyWithResponse(ctx, s.project, &api.V1CreateAFunctionParams{ - Slug: &slug, - Name: &slug, - VerifyJwt: cast.BoolToStringPtr(meta.VerifyJwt), - ImportMapPath: meta.ImportMapPath, - EntrypointPath: &meta.EntrypointPath, - EzbrSha256: &meta.SHA256, - }, eszipContentType, body) - if err != nil { - return api.BulkUpdateFunctionBody{}, errors.Errorf("failed to create function: %w", err) - } else if resp.JSON201 == nil { - return api.BulkUpdateFunctionBody{}, errors.Errorf("unexpected create function status %d: %s", resp.StatusCode(), string(resp.Body)) - } - return api.BulkUpdateFunctionBody{{ - Id: resp.JSON201.Id, - Name: resp.JSON201.Name, - Slug: resp.JSON201.Slug, - Version: resp.JSON201.Version, - EntrypointPath: resp.JSON201.EntrypointPath, - ImportMap: resp.JSON201.ImportMap, - ImportMapPath: resp.JSON201.ImportMapPath, - VerifyJwt: resp.JSON201.VerifyJwt, - Status: api.BulkUpdateFunctionBodyStatus(resp.JSON201.Status), - CreatedAt: &resp.JSON201.CreatedAt, - EzbrSha256: resp.JSON201.EzbrSha256, - }}, nil -} diff --git a/apps/cli-go/pkg/function/batch_test.go b/apps/cli-go/pkg/function/batch_test.go deleted file mode 100644 index 5bbd3a2fc1..0000000000 --- a/apps/cli-go/pkg/function/batch_test.go +++ /dev/null @@ -1,229 +0,0 @@ -package function - -import ( - "context" - "errors" - "io" - "net/http" - "testing" - - "github.com/h2non/gock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/cast" - "github.com/supabase/cli/pkg/config" -) - -type MockBundler struct { -} - -func (b *MockBundler) Bundle(ctx context.Context, slug, entrypoint, importMap string, staticFiles []string, output io.Writer) (FunctionDeployMetadata, error) { - if staticFiles == nil { - staticFiles = []string{} - } - return FunctionDeployMetadata{ - Name: &slug, - EntrypointPath: entrypoint, - ImportMapPath: &importMap, - StaticPatterns: &staticFiles, - }, nil -} - -func mockClient(t *testing.T) EdgeRuntimeAPI { - apiClient, err := api.NewClientWithResponses(mockApiHost) - require.NoError(t, err) - return NewEdgeRuntimeAPI(mockProject, *apiClient, func(era *EdgeRuntimeAPI) { - era.eszip = &MockBundler{} - }) -} - -const ( - mockApiHost = "https://api.supabase.com" - mockProject = "test-project" -) - -func TestUpsertFunctions(t *testing.T) { - client := mockClient(t) - - t.Run("deploys with bulk update", func(t *testing.T) { - // t.Cleanup(mockClock(100 * time.Millisecond)) - // Setup mock api - defer gock.OffAll() - gock.New(mockApiHost). - Get("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusOK). - JSON([]api.FunctionResponseOutput{{Slug: "test-a"}}) - gock.New(mockApiHost). - Patch("/v1/projects/" + mockProject + "/functions/test-a"). - Reply(http.StatusOK). - JSON(api.FunctionResponseOutput{Slug: "test-a"}) - gock.New(mockApiHost). - Post("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusCreated). - JSON(api.FunctionResponseOutput{Slug: "test-b"}) - gock.New(mockApiHost). - Put("/v1/projects/" + mockProject + "/functions"). - ReplyError(errors.New("network error")) - gock.New(mockApiHost). - Put("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusServiceUnavailable) - gock.New(mockApiHost). - Put("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusOK). - JSON(api.V1BulkUpdateFunctionsResponse{}) - // Run test - err := client.UpsertFunctions(context.Background(), config.FunctionConfig{ - "test-a": {Enabled: true}, - "test-b": {Enabled: true}, - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) - }) - - t.Run("skips disabled and unchanged", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New(mockApiHost). - Get("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusOK). - JSON([]api.FunctionResponseOutput{{ - Slug: "test-a", - VerifyJwt: cast.Ptr(true), - EzbrSha256: cast.Ptr("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), - }}) - // Run test - err := client.UpsertFunctions(context.Background(), config.FunctionConfig{ - "test-a": {Enabled: true, VerifyJWT: cast.Ptr(true)}, - "test-b": {Enabled: false}, - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) - }) - - t.Run("skips unchanged function when verify_jwt is not configured", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New(mockApiHost). - Get("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusOK). - JSON([]api.FunctionResponseOutput{{ - Slug: "test-a", - VerifyJwt: cast.Ptr(false), - EzbrSha256: cast.Ptr("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), - }}) - // Run test - err := client.UpsertFunctions(context.Background(), config.FunctionConfig{ - "test-a": {Enabled: true}, - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) - }) - - t.Run("handles concurrent deploy", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New(mockApiHost). - Get("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusOK). - JSON([]api.FunctionResponseOutput{}) - gock.New(mockApiHost). - Post("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusBadRequest). - BodyString("Duplicated function slug") - gock.New(mockApiHost). - Patch("/v1/projects/" + mockProject + "/functions/test"). - Reply(http.StatusOK). - JSON(api.FunctionResponseOutput{Slug: "test"}) - // Run test - err := client.UpsertFunctions(context.Background(), config.FunctionConfig{ - "test": {Enabled: true}, - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) - }) - - t.Run("retries on network failure", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New(mockApiHost). - Get("/v1/projects/" + mockProject + "/functions"). - ReplyError(errors.New("network error")) - gock.New(mockApiHost). - Get("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusServiceUnavailable) - gock.New(mockApiHost). - Get("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusBadRequest) - // Run test - err := client.UpsertFunctions(context.Background(), nil) - // Check error - assert.ErrorContains(t, err, "unexpected list functions status 400:") - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) - }) -} - -func TestCreateFunction(t *testing.T) { - client := mockClient(t) - // Setup mock api - defer gock.OffAll() - gock.New(mockApiHost). - Get("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusOK). - JSON([]api.FunctionResponseOutput{}) - gock.New(mockApiHost). - Post("/v1/projects/" + mockProject + "/functions"). - ReplyError(errors.New("network error")) - gock.New(mockApiHost). - Post("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusServiceUnavailable) - gock.New(mockApiHost). - Post("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusCreated). - JSON(api.FunctionResponseOutput{Slug: "test"}) - // Run test - err := client.UpsertFunctions(context.Background(), config.FunctionConfig{ - "test": {Enabled: true}, - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) -} - -func TestUpdateFunction(t *testing.T) { - client := mockClient(t) - // Setup mock api - defer gock.OffAll() - gock.New(mockApiHost). - Get("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusOK). - JSON([]api.FunctionResponseOutput{{Slug: "test"}}) - gock.New(mockApiHost). - Patch("/v1/projects/" + mockProject + "/functions/test"). - ReplyError(errors.New("network error")) - gock.New(mockApiHost). - Patch("/v1/projects/" + mockProject + "/functions/test"). - Reply(http.StatusServiceUnavailable) - gock.New(mockApiHost). - Patch("/v1/projects/" + mockProject + "/functions/test"). - Reply(http.StatusOK). - JSON(api.FunctionResponseOutput{Slug: "test"}) - // Run test - err := client.UpsertFunctions(context.Background(), config.FunctionConfig{ - "test": {Enabled: true}, - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) -} diff --git a/apps/cli-go/pkg/function/bundle.go b/apps/cli-go/pkg/function/bundle.go deleted file mode 100644 index 599006aef8..0000000000 --- a/apps/cli-go/pkg/function/bundle.go +++ /dev/null @@ -1,153 +0,0 @@ -package function - -import ( - "context" - "fmt" - "io" - "io/fs" - "net/url" - "os" - "os/exec" - "path/filepath" - "strings" - "time" - - "github.com/andybalholm/brotli" - "github.com/go-errors/errors" - "github.com/supabase/cli/pkg/cast" -) - -type nativeBundler struct { - tempDir string - fsys fs.FS - timeout time.Duration - denoVersion uint -} - -func NewNativeBundler(tempDir string, fsys fs.FS, opts ...func(*nativeBundler)) EszipBundler { - b := &nativeBundler{ - tempDir: tempDir, - fsys: fsys, - denoVersion: 2, - } - for _, apply := range opts { - apply(b) - } - return b -} - -func WithTimeout(timeout time.Duration) func(*nativeBundler) { - return func(b *nativeBundler) { - b.timeout = timeout - } -} - -func WithDenoVersion(version uint) func(*nativeBundler) { - return func(b *nativeBundler) { - b.denoVersion = version - } -} - -var ( - // Use a package private variable to allow testing without gosec complaining about G204 - edgeRuntimeBin = "edge-runtime" - BundleFlags = []string{} -) - -func (b *nativeBundler) Bundle(ctx context.Context, slug, entrypoint, importMap string, staticFiles []string, output io.Writer) (FunctionDeployMetadata, error) { - meta := NewMetadata(slug, entrypoint, importMap, staticFiles) - outputPath := filepath.Join(b.tempDir, slug+".eszip") - // TODO: make edge runtime write to stdout - args := []string{"bundle", "--entrypoint", entrypoint, "--output", outputPath} - if len(importMap) > 0 && !ShouldUseDenoJsonDiscovery(entrypoint, importMap) { - args = append(args, "--import-map", importMap) - } - for _, staticFile := range staticFiles { - args = append(args, "--static", staticFile) - } - args = append(args, BundleFlags...) - if b.timeout > 0 { - timeoutCtx, cancel := context.WithTimeout(ctx, b.timeout) - defer cancel() // release resources if command exits before timeout - ctx = timeoutCtx - } - cmd := exec.CommandContext(ctx, edgeRuntimeBin, args...) - cmd.Stderr = os.Stderr - cmd.Stdout = os.Stdout - if fsys, ok := b.fsys.(fs.StatFS); ok && !ShouldUsePackageJsonDiscovery(entrypoint, importMap, fsys) { - cmd.Env = append(cmd.Environ(), "DENO_NO_PACKAGE_JSON=1") - } - if err := cmd.Run(); err != nil { - return meta, errors.Errorf("failed to bundle function: %w", err) - } - defer os.Remove(outputPath) - // Compress the output - eszipBytes, err := b.fsys.Open(outputPath) - if err != nil { - return meta, errors.Errorf("failed to open eszip: %w", err) - } - defer eszipBytes.Close() - return meta, Compress(eszipBytes, output) -} - -func ShouldUseDenoJsonDiscovery(entrypoint, importMap string) bool { - return isDeno(filepath.Base(importMap)) && filepath.Dir(importMap) == filepath.Dir(entrypoint) -} - -func ShouldUsePackageJsonDiscovery(entrypoint, importMap string, fsys fs.StatFS) bool { - if len(importMap) > 0 { - return false - } - packageJsonPath := filepath.Join(filepath.Dir(entrypoint), "package.json") - if _, err := fsys.Stat(packageJsonPath); errors.Is(err, os.ErrNotExist) { - return false - } - return true -} - -const compressedEszipMagicID = "EZBR" - -func Compress(r io.Reader, w io.Writer) error { - if _, err := fmt.Fprint(w, compressedEszipMagicID); err != nil { - return errors.Errorf("failed to append magic id: %w", err) - } - brw := brotli.NewWriter(w) - defer brw.Close() - if _, err := io.Copy(brw, r); err != nil { - return errors.Errorf("failed to compress eszip: %w", err) - } - return nil -} - -func NewMetadata(slug, entrypoint, importMap string, staticFiles []string) FunctionDeployMetadata { - meta := FunctionDeployMetadata{ - Name: &slug, - EntrypointPath: toFileURL(entrypoint), - } - if len(importMap) > 0 { - meta.ImportMapPath = cast.Ptr(toFileURL(importMap)) - } - files := make([]string, len(staticFiles)) - for i, sf := range staticFiles { - files[i] = toFileURL(sf) - } - meta.StaticPatterns = &files - return meta -} - -func toFileURL(hostPath string) string { - absHostPath, err := filepath.Abs(hostPath) - if err != nil { - return hostPath - } - // Convert to unix path because edge runtime only supports linux - unixPath := toUnixPath(absHostPath) - parsed := url.URL{Scheme: "file", Path: unixPath} - return parsed.String() -} - -func toUnixPath(absHostPath string) string { - prefix := filepath.VolumeName(absHostPath) - unixPath := filepath.ToSlash(absHostPath) - return strings.TrimPrefix(unixPath, prefix) -} diff --git a/apps/cli-go/pkg/function/bundle_test.go b/apps/cli-go/pkg/function/bundle_test.go deleted file mode 100644 index 1f16b87f47..0000000000 --- a/apps/cli-go/pkg/function/bundle_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package function - -import ( - "bytes" - "context" - "fmt" - "os" - "path/filepath" - "testing" - fs "testing/fstest" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/pkg/cast" -) - -func TestMain(m *testing.M) { - // Setup mock edge runtime binary - if len(os.Args) > 1 && os.Args[1] == "bundle" { - if msg := os.Getenv("TEST_BUNDLE_ERROR"); len(msg) > 0 { - fmt.Fprintln(os.Stderr, msg) - os.Exit(1) - } - os.Exit(0) - } - // Run test suite - os.Exit(m.Run()) -} - -func TestBundleFunction(t *testing.T) { - cwd, err := os.Getwd() - require.NoError(t, err) - edgeRuntimeBin, err = os.Executable() - require.NoError(t, err) - - t.Run("creates eszip bundle", func(t *testing.T) { - var body bytes.Buffer - // Setup in-memory fs - fsys := fs.MapFS{ - "hello.eszip": &fs.MapFile{}, - } - // Setup mock bundler - bundler := nativeBundler{fsys: fsys} - // Run test - meta, err := bundler.Bundle( - context.Background(), - "hello", - "hello/index.ts", - "hello/deno.json", - []string{"hello/data.pdf"}, - &body, - ) - // Check error - assert.NoError(t, err) - assert.Equal(t, compressedEszipMagicID+";", body.String()) - assert.Equal(t, cast.Ptr("hello"), meta.Name) - entrypoint := fmt.Sprintf("file://%s/hello/index.ts", filepath.ToSlash(cwd)) - assert.Equal(t, entrypoint, meta.EntrypointPath) - importMap := fmt.Sprintf("file://%s/hello/deno.json", filepath.ToSlash(cwd)) - assert.Equal(t, &importMap, meta.ImportMapPath) - staticFile := fmt.Sprintf("file://%s/hello/data.pdf", filepath.ToSlash(cwd)) - assert.Equal(t, cast.Ptr([]string{staticFile}), meta.StaticPatterns) - assert.Nil(t, meta.VerifyJwt) - }) - - t.Run("ignores empty value", func(t *testing.T) { - var body bytes.Buffer - // Setup in-memory fs - fsys := fs.MapFS{ - "hello.eszip": &fs.MapFile{}, - } - // Setup mock bundler - bundler := nativeBundler{fsys: fsys} - // Run test - meta, err := bundler.Bundle( - context.Background(), - "hello", - "hello/index.ts", - "", - nil, - &body, - ) - // Check error - assert.NoError(t, err) - assert.Equal(t, compressedEszipMagicID+";", body.String()) - assert.Equal(t, cast.Ptr("hello"), meta.Name) - entrypoint := fmt.Sprintf("file://%s/hello/index.ts", filepath.ToSlash(cwd)) - assert.Equal(t, entrypoint, meta.EntrypointPath) - assert.Nil(t, meta.ImportMapPath) - assert.NotNil(t, meta.StaticPatterns) - assert.Nil(t, meta.VerifyJwt) - }) -} diff --git a/apps/cli-go/pkg/function/deno.go b/apps/cli-go/pkg/function/deno.go deleted file mode 100644 index 6c98b367b7..0000000000 --- a/apps/cli-go/pkg/function/deno.go +++ /dev/null @@ -1,181 +0,0 @@ -package function - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "io/fs" - "net/url" - "os" - "path" - "path/filepath" - "regexp" - "strings" - - "github.com/go-errors/errors" - "github.com/tidwall/jsonc" -) - -type ImportMap struct { - Imports map[string]string `json:"imports"` - Scopes map[string]map[string]string `json:"scopes"` - // Fallback reference for deno.json - ImportMap string `json:"importMap"` -} - -func (m *ImportMap) LoadAsDeno(imPath string, fsys fs.FS, opts ...func(string, io.Reader) error) error { - if err := m.Load(imPath, fsys, opts...); err != nil { - return err - } - if name := path.Base(imPath); isDeno(name) && m.IsReference() { - imPath = path.Join(path.Dir(imPath), m.ImportMap) - if err := m.Load(imPath, fsys, opts...); err != nil { - return err - } - } - return nil -} - -func isDeno(name string) bool { - return strings.EqualFold(name, "deno.json") || - strings.EqualFold(name, "deno.jsonc") -} - -func (m *ImportMap) IsReference() bool { - // Ref: https://github.com/denoland/deno/blob/main/cli/schemas/config-file.v1.json#L273 - return len(m.Imports) == 0 && len(m.Scopes) == 0 && len(m.ImportMap) > 0 -} - -func (m *ImportMap) Load(imPath string, fsys fs.FS, opts ...func(string, io.Reader) error) error { - data, err := fs.ReadFile(fsys, filepath.FromSlash(imPath)) - if err != nil { - return errors.Errorf("failed to load import map: %w", err) - } - if err := m.Parse(data); err != nil { - return err - } - if err := m.Resolve(imPath); err != nil { - return err - } - for _, apply := range opts { - if err := apply(imPath, bytes.NewReader(data)); err != nil { - return err - } - } - return nil -} - -func (m *ImportMap) Parse(data []byte) error { - data = jsonc.ToJSONInPlace(data) - decoder := json.NewDecoder(bytes.NewReader(data)) - if err := decoder.Decode(&m); err != nil { - return errors.Errorf("failed to parse import map: %w", err) - } - return nil -} - -func (m *ImportMap) Resolve(imPath string) error { - // Resolve all paths relative to current file - for k, v := range m.Imports { - m.Imports[k] = resolveHostPath(imPath, v) - } - for module, mapping := range m.Scopes { - for k, v := range mapping { - m.Scopes[module][k] = resolveHostPath(imPath, v) - } - } - return nil -} - -func resolveHostPath(jsonPath, hostPath string) string { - // Leave absolute paths unchanged - if path.IsAbs(hostPath) { - return hostPath - } - // Leave URLs unchanged - if parsed, err := url.Parse(hostPath); err == nil && len(parsed.Scheme) > 0 { - return hostPath - } - resolved := path.Join(path.Dir(jsonPath), hostPath) - // Directory imports need to be suffixed with / - // Ref: https://deno.com/manual@v1.33.0/basics/import_maps - if strings.HasSuffix(hostPath, "/") { - resolved += "/" - } - // Relative imports must be prefixed with ./ or ../ - if !path.IsAbs(resolved) { - resolved = "./" + resolved - } - return resolved -} - -// Ref: https://regex101.com/r/DfBdJA/1 -var importPathPattern = regexp.MustCompile(`(?i)(?:import|export)\s+(?:type\s+)?(?:{[^{}]+}|.*?)\s*(?:from)?\s*['"](.*?)['"]|import\(\s*['"](.*?)['"]\)`) - -func (importMap *ImportMap) WalkImportPaths(srcPath string, readFile func(curr string, w io.Writer) error) error { - seen := map[string]struct{}{} - // DFS because it's more efficient to pop from end of array - q := make([]string, 1) - q[0] = srcPath - for len(q) > 0 { - curr := q[len(q)-1] - q = q[:len(q)-1] - // Assume no file is symlinked - if _, ok := seen[curr]; ok { - continue - } - seen[curr] = struct{}{} - // Read into memory for regex match later - var buf bytes.Buffer - if err := readFile(curr, &buf); errors.Is(err, os.ErrNotExist) { - fmt.Fprintln(os.Stderr, "WARN:", err) - continue - } else if err != nil { - return err - } - // Traverse all modules imported by the current source file - for _, matches := range importPathPattern.FindAllStringSubmatch(buf.String(), -1) { - if len(matches) < 3 { - continue - } - // Matches 'from' clause if present, else fallback to 'import' - mod := matches[1] - if len(mod) == 0 { - mod = matches[2] - } - mod = strings.TrimSpace(mod) - // Substitute kv from import map - substituted := false - for k, v := range importMap.Imports { - if strings.HasPrefix(mod, k) { - mod = v + mod[len(k):] - substituted = true - } - } - // Ignore URLs and directories, assuming no sloppy imports - // https://github.com/denoland/deno/issues/2506#issuecomment-2727635545 - if len(path.Ext(mod)) == 0 { - continue - } - // Deno import path must begin with one of these prefixes - if !isRelPath(mod) && !isAbsPath(mod) { - continue - } - if isRelPath(mod) && !substituted { - mod = path.Join(path.Dir(curr), mod) - } - // Cleans import path to help detect duplicates - q = append(q, path.Clean(mod)) - } - } - return nil -} - -func isRelPath(mod string) bool { - return strings.HasPrefix(mod, "./") || strings.HasPrefix(mod, "../") -} - -func isAbsPath(mod string) bool { - return strings.HasPrefix(mod, "/") -} diff --git a/apps/cli-go/pkg/function/deno_test.go b/apps/cli-go/pkg/function/deno_test.go deleted file mode 100644 index aa9677f5ad..0000000000 --- a/apps/cli-go/pkg/function/deno_test.go +++ /dev/null @@ -1,216 +0,0 @@ -package function - -import ( - "embed" - "io" - "os" - "testing" - fs "testing/fstest" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -//go:embed testdata -var testImports embed.FS - -type MockFS struct { - mock.Mock -} - -func (m *MockFS) ReadFile(srcPath string, w io.Writer) error { - _ = m.Called(srcPath) - data, err := testImports.ReadFile(srcPath) - if err != nil { - return err - } - if _, err := w.Write(data); err != nil { - return err - } - return nil -} - -func TestImportPaths(t *testing.T) { - t.Run("iterates all import paths", func(t *testing.T) { - // Setup in-memory fs - fsys := MockFS{} - fsys.On("ReadFile", "/modules/my-module.ts").Once() - fsys.On("ReadFile", "testdata/modules/imports.ts").Once() - fsys.On("ReadFile", "testdata/geometries/Geometries.js").Once() - // Run test - im := ImportMap{} - err := im.WalkImportPaths("testdata/modules/imports.ts", fsys.ReadFile) - // Check error - assert.NoError(t, err) - fsys.AssertExpectations(t) - }) - - t.Run("iterates with import map", func(t *testing.T) { - // Setup in-memory fs - fsys := MockFS{} - fsys.On("ReadFile", "/modules/my-module.ts").Once() - fsys.On("ReadFile", "testdata/modules/imports.ts").Once() - fsys.On("ReadFile", "testdata/geometries/Geometries.js").Once() - fsys.On("ReadFile", "testdata/shared/whatever.ts").Once() - fsys.On("ReadFile", "testdata/shared/mod.ts").Once() - fsys.On("ReadFile", "testdata/nested/index.ts").Once() - // Setup deno.json - im := ImportMap{Imports: map[string]string{ - "module-name/": "../shared/", - }} - assert.NoError(t, im.Resolve("testdata/modules/deno.json")) - // Run test - err := im.WalkImportPaths("testdata/modules/imports.ts", fsys.ReadFile) - // Check error - assert.NoError(t, err) - fsys.AssertExpectations(t) - }) - - t.Run("resolves legacy import map", func(t *testing.T) { - // Setup in-memory fs - fsys := MockFS{} - fsys.On("ReadFile", "/modules/my-module.ts").Once() - fsys.On("ReadFile", "testdata/modules/imports.ts").Once() - fsys.On("ReadFile", "testdata/geometries/Geometries.js").Once() - fsys.On("ReadFile", "testdata/shared/whatever.ts").Once() - fsys.On("ReadFile", "testdata/shared/mod.ts").Once() - fsys.On("ReadFile", "testdata/nested/index.ts").Once() - // Setup legacy import map - im := ImportMap{Imports: map[string]string{ - "module-name/": "./shared/", - }} - assert.NoError(t, im.Resolve("testdata/import_map.json")) - // Run test - err := im.WalkImportPaths("testdata/modules/imports.ts", fsys.ReadFile) - // Check error - assert.NoError(t, err) - fsys.AssertExpectations(t) - }) - - t.Run("iterates multiline import type statements", func(t *testing.T) { - // This test verifies that multiline import type statements are correctly parsed. - // The (?:type\s+)? group consumes the type keyword so braced imports hit the {[^{}]+} branch. - // Setup in-memory fs - fsys := MockFS{} - fsys.On("ReadFile", "testdata/modules/import_types.ts").Once() - fsys.On("ReadFile", "testdata/types/database.ts").Once() - // Run test - im := ImportMap{} - err := im.WalkImportPaths("testdata/modules/import_types.ts", fsys.ReadFile) - // Check error - assert.NoError(t, err) - fsys.AssertExpectations(t) - }) -} - -func TestResolveImports(t *testing.T) { - t.Run("resolves relative directory", func(t *testing.T) { - imPath := "supabase/functions/import_map.json" - // Setup in-memory fs - fsys := fs.MapFS{ - imPath: &fs.MapFile{Data: []byte(`{ - "imports": { - "abs/": "/tmp/", - "root": "../../common", - "parent": "../tests", - "child": "child/", - "missing": "../missing" - } - }`)}, - "/tmp/": &fs.MapFile{}, - "common": &fs.MapFile{}, - "supabase/tests": &fs.MapFile{}, - "supabase/functions/child": &fs.MapFile{}, - } - // Run test - resolved := ImportMap{} - err := resolved.Load(imPath, fsys) - // Check error - assert.NoError(t, err) - assert.Equal(t, "/tmp/", resolved.Imports["abs/"]) - assert.Equal(t, "./common", resolved.Imports["root"]) - assert.Equal(t, "./supabase/tests", resolved.Imports["parent"]) - assert.Equal(t, "./supabase/functions/child/", resolved.Imports["child"]) - assert.Equal(t, "./supabase/missing", resolved.Imports["missing"]) - }) - - t.Run("resolves parent scopes", func(t *testing.T) { - imPath := "supabase/functions/import_map.json" - // Setup in-memory fs - fsys := fs.MapFS{ - imPath: &fs.MapFile{Data: []byte(`{ - "scopes": { - "my-scope": { - "my-mod": "https://deno.land" - } - } - }`)}, - } - // Run test - resolved := ImportMap{} - err := resolved.Load(imPath, fsys) - // Check error - assert.NoError(t, err) - assert.Equal(t, "https://deno.land", resolved.Scopes["my-scope"]["my-mod"]) - }) -} - -func TestResolveDeno(t *testing.T) { - t.Run("resolves deno.json", func(t *testing.T) { - imPath := "supabase/functions/slug/deno.json" - // Setup in-memory fs - fsys := fs.MapFS{ - imPath: &fs.MapFile{Data: []byte(`{ - "imports": { - "@mod": "./mod.ts" - }, - "importMap": "../../import_map.json" - }`)}, - "supabase/functions/slug/mod.ts": &fs.MapFile{}, - } - // Run test - resolved := ImportMap{} - err := resolved.LoadAsDeno(imPath, fsys) - // Check error - assert.NoError(t, err) - assert.Equal(t, "./supabase/functions/slug/mod.ts", resolved.Imports["@mod"]) - }) - - t.Run("resolves fallback imports", func(t *testing.T) { - imPath := "supabase/functions/slug/deno.json" - // Setup in-memory fs - fsys := fs.MapFS{ - imPath: &fs.MapFile{Data: []byte(`{ - "importMap": "../../import_map.json" - }`)}, - "supabase/import_map.json": &fs.MapFile{Data: []byte(`{ - "imports": { - "my-mod": "https://deno.land" - } - }`)}, - } - // Run test - resolved := ImportMap{} - err := resolved.LoadAsDeno(imPath, fsys) - // Check error - assert.NoError(t, err) - assert.Equal(t, "https://deno.land", resolved.Imports["my-mod"]) - }) - - t.Run("throws error on missing import", func(t *testing.T) { - imPath := "supabase/functions/slug/deno.jsonc" - // Setup in-memory fs - fsys := fs.MapFS{ - imPath: &fs.MapFile{Data: []byte(`{ - "importMap": "../../import_map.json" - }`)}, - } - // Run test - resolved := ImportMap{} - err := resolved.LoadAsDeno(imPath, fsys) - // Check error - assert.ErrorIs(t, err, os.ErrNotExist) - assert.Empty(t, resolved.Imports) - assert.Empty(t, resolved.Scopes) - }) -} diff --git a/apps/cli-go/pkg/function/deploy.go b/apps/cli-go/pkg/function/deploy.go deleted file mode 100644 index 38458d6cdb..0000000000 --- a/apps/cli-go/pkg/function/deploy.go +++ /dev/null @@ -1,304 +0,0 @@ -package function - -import ( - "context" - "encoding/json" - "fmt" - "io" - "io/fs" - "mime/multipart" - "net/http" - "os" - "path/filepath" - "strconv" - "time" - - "github.com/go-errors/errors" - "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/cast" - "github.com/supabase/cli/pkg/config" - "github.com/supabase/cli/pkg/queue" -) - -var ErrNoDeploy = errors.New("All Functions are up to date.") - -const deployRateLimitMaxRetries = 8 - -func (s *EdgeRuntimeAPI) Deploy(ctx context.Context, functionConfig config.FunctionConfig, fsys fs.FS) error { - if s.eszip != nil { - return s.UpsertFunctions(ctx, functionConfig) - } - remoteFunctions, err := s.listRemoteFunctionsForVerifyJwt(ctx, functionConfig) - if err != nil { - return err - } - // Convert all paths in functions config to relative when using api deploy - var toDeploy []FunctionDeployMetadata - for slug, fc := range functionConfig { - if !fc.Enabled { - fmt.Fprintln(os.Stderr, "Skipping disabled Function:", slug) - continue - } - meta := FunctionDeployMetadata{ - Name: &slug, - EntrypointPath: toRelPath(fc.Entrypoint), - ImportMapPath: cast.Ptr(toRelPath(fc.ImportMap)), - VerifyJwt: fc.VerifyJWT, - } - if meta.VerifyJwt == nil { - if remote, ok := remoteFunctions[slug]; ok { - meta.VerifyJwt = remote.VerifyJwt - } - } - files := make([]string, len(fc.StaticFiles)) - for i, sf := range fc.StaticFiles { - files[i] = toRelPath(sf) - } - meta.StaticPatterns = &files - toDeploy = append(toDeploy, meta) - } - if len(toDeploy) == 0 { - return errors.New(ErrNoDeploy) - } else if len(toDeploy) == 1 { - param := api.V1DeployAFunctionParams{Slug: toDeploy[0].Name} - _, err := s.upload(ctx, param, toDeploy[0], fsys) - return err - } - return s.bulkUpload(ctx, toDeploy, fsys) -} - -func (s *EdgeRuntimeAPI) listRemoteFunctionsForVerifyJwt(ctx context.Context, functionConfig config.FunctionConfig) (map[string]api.FunctionResponseOutput, error) { - needsRemote := false - for _, fc := range functionConfig { - if fc.Enabled && fc.VerifyJWT == nil { - needsRemote = true - break - } - } - if !needsRemote { - return nil, nil - } - resp, err := s.client.V1ListAllFunctionsWithResponse(ctx, s.project) - if err != nil { - return nil, errors.Errorf("failed to list functions: %w", err) - } else if resp.JSON200 == nil { - return nil, errors.Errorf("unexpected list functions status %d: %s", resp.StatusCode(), string(resp.Body)) - } - remoteFunctions := make(map[string]api.FunctionResponseOutput, len(*resp.JSON200)) - for _, function := range *resp.JSON200 { - remoteFunctions[function.Slug] = function - } - return remoteFunctions, nil -} - -func toRelPath(fp string) string { - if filepath.IsAbs(fp) { - if cwd, err := os.Getwd(); err == nil { - if relPath, err := filepath.Rel(cwd, fp); err == nil { - fp = relPath - } - } - } - return filepath.ToSlash(fp) -} - -func (s *EdgeRuntimeAPI) bulkUpload(ctx context.Context, toDeploy []FunctionDeployMetadata, fsys fs.FS) error { - jq := queue.NewJobQueue(s.maxJobs) - toUpdate := make(api.BulkUpdateFunctionBody, len(toDeploy)) - // Bulk uploads only write the bundle and bump the version remotely, so the new metadata - // exists nowhere but this client until the final bulk update request persists it. Bailing - // out on the first upload error strands that metadata and makes every subsequent deploy - // fail with a version conflict, so we keep going and report the errors at the end. - // - // Upload errors are recorded per function index rather than surfaced through the job - // queue's error channel, so they are reported in input order regardless of completion - // order under concurrent --jobs, matching the TS port. - uploadErrs := make([]error, len(toDeploy)) - var queueErrs []error - for i, meta := range toDeploy { - param := api.V1DeployAFunctionParams{ - Slug: meta.Name, - BundleOnly: cast.Ptr("true"), - } - bundle := func() error { - fmt.Fprintln(os.Stderr, "Deploying Function:", *meta.Name) - resp, err := s.upload(ctx, param, meta, fsys) - if err != nil { - uploadErrs[i] = err - return nil - } - toUpdate[i].Id = resp.Id - toUpdate[i].Name = resp.Name - toUpdate[i].Slug = resp.Slug - toUpdate[i].Version = resp.Version - toUpdate[i].EntrypointPath = resp.EntrypointPath - toUpdate[i].ImportMap = resp.ImportMap - toUpdate[i].ImportMapPath = resp.ImportMapPath - toUpdate[i].VerifyJwt = resp.VerifyJwt - toUpdate[i].Status = api.BulkUpdateFunctionBodyStatus(resp.Status) - toUpdate[i].CreatedAt = resp.CreatedAt - return nil - } - queueErrs = append(queueErrs, jq.Put(bundle)) - } - queueErrs = append(queueErrs, jq.Collect()) - bundleErr := errors.Join(append(uploadErrs, queueErrs...)...) - // Failed uploads leave their slot zero valued, ie. without a function id. - uploaded := make(api.BulkUpdateFunctionBody, 0, len(toUpdate)) - for _, f := range toUpdate { - if len(f.Id) > 0 { - uploaded = append(uploaded, f) - } - } - if len(uploaded) == 0 { - return bundleErr - } - for attempt := 0; ; attempt++ { - resp, err := s.client.V1BulkUpdateFunctionsWithResponse(ctx, s.project, uploaded) - if err != nil { - return errors.Join(bundleErr, errors.Errorf("failed to bulk update: %w", err)) - } else if resp.JSON200 != nil { - return bundleErr - } else if resp.StatusCode() != http.StatusTooManyRequests || attempt >= deployRateLimitMaxRetries { - return errors.Join(bundleErr, errors.Errorf("unexpected bulk update status %d: %s", resp.StatusCode(), string(resp.Body))) - } else if err := waitForRateLimit(ctx, responseHeaders(resp.HTTPResponse), attempt, "bulk updating functions"); err != nil { - return errors.Join(bundleErr, err) - } - } -} - -func (s *EdgeRuntimeAPI) upload(ctx context.Context, param api.V1DeployAFunctionParams, meta FunctionDeployMetadata, fsys fs.FS) (*api.DeployFunctionResponseOutput, error) { - for attempt := 0; ; attempt++ { - resp, err := s.uploadOnce(ctx, param, meta, fsys) - if resp != nil && resp.JSON201 != nil { - return resp.JSON201, nil - } else if err != nil { - return nil, err - } else if resp.StatusCode() != http.StatusTooManyRequests || attempt >= deployRateLimitMaxRetries { - return nil, errors.Errorf("unexpected deploy status %d: %s", resp.StatusCode(), string(resp.Body)) - } else if err := waitForRateLimit(ctx, responseHeaders(resp.HTTPResponse), attempt, "deploying function "+cast.Val(meta.Name, "")); err != nil { - return nil, err - } - } -} - -func (s *EdgeRuntimeAPI) uploadOnce(ctx context.Context, param api.V1DeployAFunctionParams, meta FunctionDeployMetadata, fsys fs.FS) (*api.V1DeployAFunctionResponse, error) { - body, w := io.Pipe() - form := multipart.NewWriter(w) - ctx, cancel := context.WithCancelCause(ctx) - go func() { - defer w.Close() - defer form.Close() - if err := writeForm(form, meta, fsys); err != nil { - // Since we are streaming files to the POST request body, any errors - // should be propagated to the request context to cancel the upload. - cancel(err) - } - }() - resp, err := s.client.V1DeployAFunctionWithBodyWithResponse(ctx, s.project, ¶m, form.FormDataContentType(), body) - if cause := context.Cause(ctx); cause != ctx.Err() { - return nil, cause - } else if err != nil { - return nil, errors.Errorf("failed to deploy function: %w", err) - } - return resp, nil -} - -func waitForRateLimit(ctx context.Context, headers http.Header, attempt int, action string) error { - delay := rateLimitDelay(headers, attempt) - fmt.Fprintf(os.Stderr, "Rate limit exceeded while %s. Retrying in %s.\n", action, delay.Round(time.Second)) - timer := time.NewTimer(delay) - defer timer.Stop() - select { - case <-ctx.Done(): - return ctx.Err() - case <-timer.C: - return nil - } -} - -func responseHeaders(resp *http.Response) http.Header { - if resp == nil { - return nil - } - return resp.Header -} - -func rateLimitDelay(headers http.Header, attempt int) time.Duration { - if delay, ok := parseRateLimitDelay(headers.Get("Retry-After")); ok { - return delay - } - if delay, ok := parseRateLimitDelay(headers.Get("X-RateLimit-Reset")); ok { - return delay - } - delay := time.Second << min(attempt, 5) - return delay -} - -func parseRateLimitDelay(value string) (time.Duration, bool) { - if len(value) == 0 { - return 0, false - } - if seconds, err := strconv.Atoi(value); err == nil { - return max(time.Duration(seconds)*time.Second, 0), true - } - if retryAt, err := http.ParseTime(value); err == nil { - return max(time.Until(retryAt), 0), true - } - return 0, false -} - -func writeForm(form *multipart.Writer, meta FunctionDeployMetadata, fsys fs.FS) error { - m, err := form.CreateFormField("metadata") - if err != nil { - return errors.Errorf("failed to create metadata: %w", err) - } - enc := json.NewEncoder(m) - if err := enc.Encode(meta); err != nil { - return errors.Errorf("failed to encode metadata: %w", err) - } - uploadAsset := func(srcPath string, r io.Reader) error { - fmt.Fprintf(os.Stderr, "Uploading asset (%s): %s\n", *meta.Name, srcPath) - f, err := form.CreateFormFile("file", srcPath) - if err != nil { - return errors.Errorf("failed to create form: %w", err) - } - if _, err := io.Copy(f, r); err != nil { - return errors.Errorf("failed to write form: %w", err) - } - return nil - } - addFile := func(srcPath string, w io.Writer) error { - f, err := fsys.Open(filepath.FromSlash(srcPath)) - if err != nil { - return errors.Errorf("failed to read file: %w", err) - } - defer f.Close() - if fi, err := f.Stat(); err != nil { - return errors.Errorf("failed to stat file: %w", err) - } else if fi.IsDir() { - return errors.New("file path is a directory: " + srcPath) - } - r := io.TeeReader(f, w) - return uploadAsset(srcPath, r) - } - // Add import map - importMap := ImportMap{} - if imPath := cast.Val(meta.ImportMapPath, ""); len(imPath) > 0 { - if err := importMap.LoadAsDeno(imPath, fsys, uploadAsset); err != nil { - return err - } - } - // Add static files - patterns := config.Glob(cast.Val(meta.StaticPatterns, []string{})) - files, err := patterns.Files(fsys) - if err != nil { - fmt.Fprintln(os.Stderr, "WARN:", err) - } - for _, sfPath := range files { - if err := addFile(sfPath, io.Discard); err != nil { - return err - } - } - return importMap.WalkImportPaths(meta.EntrypointPath, addFile) -} diff --git a/apps/cli-go/pkg/function/deploy_test.go b/apps/cli-go/pkg/function/deploy_test.go deleted file mode 100644 index 06821e3ac1..0000000000 --- a/apps/cli-go/pkg/function/deploy_test.go +++ /dev/null @@ -1,448 +0,0 @@ -package function - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "io" - "mime/multipart" - "net/http" - "os" - "path" - "strings" - "testing" - fs "testing/fstest" - "time" - - "github.com/h2non/gock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/cast" - "github.com/supabase/cli/pkg/config" -) - -func assertFormEqual(t *testing.T, actual []byte) { - snapshot := path.Join("testdata", path.Base(t.Name())+".form") - expected, err := testImports.ReadFile(snapshot) - if errors.Is(err, os.ErrNotExist) { - assert.NoError(t, os.WriteFile(snapshot, actual, 0600)) - } - assert.Equal(t, string(expected), string(actual)) -} - -// captureBody records the request body without consuming it, so tests can assert -// on the exact payload sent to a mocked endpoint. -func captureBody(out *[]byte) gock.MatchFunc { - return func(req *http.Request, _ *gock.Request) (bool, error) { - if req.Body == nil { - return true, nil - } - body, err := io.ReadAll(req.Body) - if err != nil { - return false, err - } - req.Body = io.NopCloser(bytes.NewReader(body)) - *out = body - return true, nil - } -} - -func mockFunctionList(functions ...api.FunctionResponseOutput) { - gock.New(mockApiHost). - Get("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusOK). - JSON(functions) -} - -func TestWriteForm(t *testing.T) { - t.Run("writes import map", func(t *testing.T) { - var buf bytes.Buffer - form := multipart.NewWriter(&buf) - require.NoError(t, form.SetBoundary("test")) - // Setup in-memory fs - fsys := testImports - // Run test - err := writeForm(form, FunctionDeployMetadata{ - Name: cast.Ptr("nested"), - VerifyJwt: cast.Ptr(true), - EntrypointPath: "testdata/nested/index.ts", - ImportMapPath: cast.Ptr("testdata/nested/deno.json"), - StaticPatterns: cast.Ptr([]string{"testdata/*/*.js"}), - }, fsys) - // Check error - assert.NoError(t, err) - assertFormEqual(t, buf.Bytes()) - }) - - t.Run("throws error on missing file", func(t *testing.T) { - var buf bytes.Buffer - form := multipart.NewWriter(&buf) - require.NoError(t, form.SetBoundary("test")) - // Setup in-memory fs - fsys := fs.MapFS{} - // Run test - err := writeForm(form, FunctionDeployMetadata{ - ImportMapPath: cast.Ptr("testdata/import_map.json"), - }, fsys) - // Check error - assert.ErrorIs(t, err, os.ErrNotExist) - }) - - t.Run("throws error on directory path", func(t *testing.T) { - var buf bytes.Buffer - form := multipart.NewWriter(&buf) - require.NoError(t, form.SetBoundary("test")) - // Setup in-memory fs - fsys := testImports - // Run test - err := writeForm(form, FunctionDeployMetadata{ - StaticPatterns: cast.Ptr([]string{"testdata"}), - }, fsys) - // Check error - assert.ErrorContains(t, err, "file path is a directory:") - }) -} - -func TestDeployAll(t *testing.T) { - apiClient, err := api.NewClientWithResponses(mockApiHost) - require.NoError(t, err) - client := NewEdgeRuntimeAPI(mockProject, *apiClient) - - t.Run("deploys single slug", func(t *testing.T) { - c := config.FunctionConfig{"demo": { - Enabled: true, - Entrypoint: "testdata/shared/whatever.ts", - }} - // Setup in-memory fs - fsys := testImports - // Setup mock api - defer gock.OffAll() - mockFunctionList() - gock.New(mockApiHost). - Post("/v1/projects/"+mockProject+"/functions/deploy"). - MatchParam("slug", "demo"). - Reply(http.StatusCreated). - JSON(api.DeployFunctionResponseOutput{}) - // Run test - err := client.Deploy(context.Background(), c, fsys) - // Check error - assert.NoError(t, err) - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) - }) - - t.Run("retries single slug after rate limit", func(t *testing.T) { - c := config.FunctionConfig{"demo": { - Enabled: true, - Entrypoint: "testdata/shared/whatever.ts", - }} - // Setup in-memory fs - fsys := testImports - // Setup mock api - defer gock.OffAll() - mockFunctionList() - gock.New(mockApiHost). - Post("/v1/projects/"+mockProject+"/functions/deploy"). - MatchParam("slug", "demo"). - Reply(http.StatusTooManyRequests). - SetHeader("Retry-After", "0"). - JSON(map[string]string{"message": "Too Many Requests"}) - gock.New(mockApiHost). - Post("/v1/projects/"+mockProject+"/functions/deploy"). - MatchParam("slug", "demo"). - Reply(http.StatusCreated). - JSON(api.DeployFunctionResponseOutput{}) - // Run test - err := client.Deploy(context.Background(), c, fsys) - // Check error - assert.NoError(t, err) - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) - }) - - t.Run("deploys multiple slugs", func(t *testing.T) { - c := config.FunctionConfig{ - "test-ts": { - Enabled: true, - Entrypoint: "testdata/shared/whatever.ts", - }, - "test-js": { - Enabled: true, - Entrypoint: "testdata/geometries/Geometries.js", - }, - } - // Setup in-memory fs - fsys := testImports - // Setup mock api - defer gock.OffAll() - mockFunctionList() - for slug := range c { - gock.New(mockApiHost). - Post("/v1/projects/"+mockProject+"/functions/deploy"). - MatchParam("slug", slug). - Reply(http.StatusCreated). - JSON(api.DeployFunctionResponseOutput{Id: slug}) - } - gock.New(mockApiHost). - Put("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusOK). - JSON(api.BulkUpdateFunctionResponseOutput{}) - // Run test - err := client.Deploy(context.Background(), c, fsys) - // Check error - assert.NoError(t, err) - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) - }) - - t.Run("retries bulk update after rate limit reset", func(t *testing.T) { - c := config.FunctionConfig{ - "test-ts": { - Enabled: true, - Entrypoint: "testdata/shared/whatever.ts", - }, - "test-js": { - Enabled: true, - Entrypoint: "testdata/geometries/Geometries.js", - }, - } - // Setup in-memory fs - fsys := testImports - // Setup mock api - defer gock.OffAll() - mockFunctionList() - for slug := range c { - gock.New(mockApiHost). - Post("/v1/projects/"+mockProject+"/functions/deploy"). - MatchParam("slug", slug). - Reply(http.StatusCreated). - JSON(api.DeployFunctionResponseOutput{Id: slug}) - } - gock.New(mockApiHost). - Put("/v1/projects/"+mockProject+"/functions"). - Reply(http.StatusTooManyRequests). - SetHeader("X-RateLimit-Reset", "0"). - JSON(map[string]string{"message": "Too Many Requests"}) - gock.New(mockApiHost). - Put("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusOK). - JSON(api.BulkUpdateFunctionResponseOutput{}) - // Run test - err := client.Deploy(context.Background(), c, fsys) - // Check error - assert.NoError(t, err) - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) - }) - - t.Run("bulk updates successful uploads when one fails", func(t *testing.T) { - c := config.FunctionConfig{ - "test-ts": { - Enabled: true, - Entrypoint: "testdata/shared/whatever.ts", - }, - "test-js": { - Enabled: true, - Entrypoint: "testdata/geometries/Geometries.js", - }, - } - // Setup in-memory fs - fsys := testImports - // Setup mock api - defer gock.OffAll() - mockFunctionList() - gock.New(mockApiHost). - Post("/v1/projects/"+mockProject+"/functions/deploy"). - MatchParam("slug", "test-ts"). - Reply(http.StatusCreated). - JSON(api.DeployFunctionResponseOutput{Id: "test-ts", Name: "test-ts", Slug: "test-ts"}) - gock.New(mockApiHost). - Post("/v1/projects/"+mockProject+"/functions/deploy"). - MatchParam("slug", "test-js"). - Reply(http.StatusConflict). - JSON(map[string]string{"message": "deployment already exists"}) - var bulkBody []byte - gock.New(mockApiHost). - Put("/v1/projects/" + mockProject + "/functions"). - AddMatcher(captureBody(&bulkBody)). - Reply(http.StatusOK). - JSON(api.BulkUpdateFunctionResponseOutput{}) - // Run test - err := client.Deploy(context.Background(), c, fsys) - // Check error - assert.ErrorContains(t, err, "unexpected deploy status 409") - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) - var toUpdate api.BulkUpdateFunctionBody - require.NoError(t, json.Unmarshal(bulkBody, &toUpdate)) - require.Len(t, toUpdate, 1) - assert.Equal(t, "test-ts", toUpdate[0].Slug) - }) - - t.Run("skips bulk update when all uploads fail", func(t *testing.T) { - c := config.FunctionConfig{ - "test-ts": { - Enabled: true, - Entrypoint: "testdata/shared/whatever.ts", - }, - "test-js": { - Enabled: true, - Entrypoint: "testdata/geometries/Geometries.js", - }, - } - // Setup in-memory fs - fsys := testImports - // Setup mock api - defer gock.OffAll() - mockFunctionList() - for slug := range c { - gock.New(mockApiHost). - Post("/v1/projects/"+mockProject+"/functions/deploy"). - MatchParam("slug", slug). - Reply(http.StatusConflict). - JSON(map[string]string{"message": "deployment already exists"}) - } - // Run test - err := client.Deploy(context.Background(), c, fsys) - // Check error - assert.ErrorContains(t, err, "unexpected deploy status 409") - // No bulk update is mocked, so any PUT would show up as unmatched - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) - }) - - t.Run("reports upload and bulk update failures together", func(t *testing.T) { - c := config.FunctionConfig{ - "test-ts": { - Enabled: true, - Entrypoint: "testdata/shared/whatever.ts", - }, - "test-js": { - Enabled: true, - Entrypoint: "testdata/geometries/Geometries.js", - }, - } - // Setup in-memory fs - fsys := testImports - // Setup mock api - defer gock.OffAll() - mockFunctionList() - gock.New(mockApiHost). - Post("/v1/projects/"+mockProject+"/functions/deploy"). - MatchParam("slug", "test-ts"). - Reply(http.StatusCreated). - JSON(api.DeployFunctionResponseOutput{Id: "test-ts", Name: "test-ts", Slug: "test-ts"}) - gock.New(mockApiHost). - Post("/v1/projects/"+mockProject+"/functions/deploy"). - MatchParam("slug", "test-js"). - Reply(http.StatusConflict). - JSON(map[string]string{"message": "deployment already exists"}) - gock.New(mockApiHost). - Put("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusBadRequest). - JSON(map[string]string{"message": "bulk update rejected"}) - // Run test - err := client.Deploy(context.Background(), c, fsys) - // Check error - assert.ErrorContains(t, err, "unexpected deploy status 409") - assert.ErrorContains(t, err, "unexpected bulk update status 400") - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) - }) - - t.Run("reports concurrent upload failures in input order", func(t *testing.T) { - client2 := NewEdgeRuntimeAPI(mockProject, *apiClient, WithMaxJobs(2)) - toDeploy := []FunctionDeployMetadata{ - { - Name: cast.Ptr("first-fn"), - EntrypointPath: "testdata/shared/whatever.ts", - }, - { - Name: cast.Ptr("second-fn"), - EntrypointPath: "testdata/geometries/Geometries.js", - }, - } - fsys := testImports - // Setup mock api - defer gock.OffAll() - gock.New(mockApiHost). - Post("/v1/projects/"+mockProject+"/functions/deploy"). - MatchParam("slug", "first-fn"). - Reply(http.StatusConflict). - Delay(500 * time.Millisecond). - JSON(map[string]string{"message": "first failed"}) - gock.New(mockApiHost). - Post("/v1/projects/"+mockProject+"/functions/deploy"). - MatchParam("slug", "second-fn"). - Reply(http.StatusConflict). - JSON(map[string]string{"message": "second failed"}) - // Run test - err := client2.bulkUpload(context.Background(), toDeploy, fsys) - // Check error - require.Error(t, err) - // JSON replies are newline-terminated by json.Encoder, so filter out the - // resulting blank lines between joined errors before counting entries. - var lines []string - for _, line := range strings.Split(err.Error(), "\n") { - if len(line) > 0 { - lines = append(lines, line) - } - } - require.Len(t, lines, 2) - assert.Contains(t, lines[0], "first failed") - assert.Contains(t, lines[1], "second failed") - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) - }) - - t.Run("preserves remote verify_jwt when not configured", func(t *testing.T) { - c := config.FunctionConfig{"demo": { - Enabled: true, - Entrypoint: "testdata/shared/whatever.ts", - }} - // Setup in-memory fs - fsys := testImports - // Setup mock api - defer gock.OffAll() - mockFunctionList(api.FunctionResponseOutput{ - Id: "demo", - Name: "demo", - Slug: "demo", - VerifyJwt: cast.Ptr(false), - }) - gock.New(mockApiHost). - Post("/v1/projects/"+mockProject+"/functions/deploy"). - MatchParam("slug", "demo"). - BodyString(`"verify_jwt":false`). - Reply(http.StatusCreated). - JSON(api.DeployFunctionResponseOutput{}) - // Run test - err := client.Deploy(context.Background(), c, fsys) - // Check error - assert.NoError(t, err) - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) - }) - - t.Run("throws error on network failure", func(t *testing.T) { - errNetwork := errors.New("network") - c := config.FunctionConfig{"demo": {Enabled: true}} - // Setup in-memory fs - fsys := fs.MapFS{} - // Setup mock api - defer gock.OffAll() - mockFunctionList() - gock.New(mockApiHost). - Post("/v1/projects/"+mockProject+"/functions/deploy"). - MatchParam("slug", "demo"). - ReplyError(errNetwork) - // Run test - err := client.Deploy(context.Background(), c, fsys) - // Check error - assert.ErrorIs(t, err, errNetwork) - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) - }) -} diff --git a/apps/cli-go/pkg/function/testdata/geometries/Geometries.js b/apps/cli-go/pkg/function/testdata/geometries/Geometries.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/cli-go/pkg/function/testdata/modules/import_types.ts b/apps/cli-go/pkg/function/testdata/modules/import_types.ts deleted file mode 100644 index d285ab0ee8..0000000000 --- a/apps/cli-go/pkg/function/testdata/modules/import_types.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Test file for multiline import type statements -// This pattern requires (?:type\s+)? to route braced imports into the {[^{}]+} branch - -// Multiline import type - should be matched by the regex -import type { - Database, - Json -} from '../types/database.ts' - -// Single line import type - should also work -import type { Database as DB } from '../types/database.ts' - -// Re-export type to verify export pattern -export type { Database } from '../types/database.ts' - -// Multiline export type -export type { - Json -} from '../types/database.ts' - -// Non-braced default type import - exercises the .*? branch on single-line -import type Database from '../types/database.ts' diff --git a/apps/cli-go/pkg/function/testdata/modules/imports.ts b/apps/cli-go/pkg/function/testdata/modules/imports.ts deleted file mode 100644 index d4d8cda7b3..0000000000 --- a/apps/cli-go/pkg/function/testdata/modules/imports.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - Component -} from '@angular2/core'; -import defaultMember from "module-name"; -import * as name from "module-name "; -import { member } from " module-name"; -import { member as alias } from "module-name"; -import { member1 , member2 } from "module-name"; -import { member1 , member2 as alias2 , member3 as alias3 } from "module-name"; -import { - Component -} from '@angular2/core'; -import defaultMember from "$module-name"; -import defaultMember, { member, member } from "module-name"; -import defaultMember, * as name from "module-name"; - -import * as name from "module-name " -import { member } from " module-name" -import { member as alias } from "module-name" -import { member1 , member2 } from "module-name" -import { member1 , member2 as alias2 , member3 as alias3 } from "module-name" -import { - Component -} from '@angular2/core' -import defaultMember from "$module-name" -import defaultMember, { member, member } from "module-name" -import defaultMember, * as name from "module-name" - -import "module-name"; -import React from "react" -import { Field } from "redux-form" -import "module-name"; - -import { - PlaneBufferGeometry, - OctahedronGeometry, - TorusBufferGeometry -} from '../geometries/Geometries.js'; - -import { - PlaneBufferGeometry, - OctahedronGeometry, - TorusBufferGeometry -} from '../geometries/Geometries.js' - -import("module-name/whatever.ts"); -import("module-name/whatever.ts") - -import { Field } from "redux-form"; -import MultiContentListView from "./views/ListView"; -import MultiContentAddView from "./views/AddView"; -import MultiContentEditView from "./views/EditView"; - -import { Field } from "redux-form" -import MultiContentListView from "./views/ListView" -import MultiContentAddView from "./views/AddView" -import MultiContentEditView from "./views/EditView" - - -// JSX-like snippets are comments so this .ts fixture remains valid syntax. -// -// - -// *Add all needed dependency to this module -// *app requires those import modules to function - - -/** -* - *Add all needed dependency to this module - *app requires those import modules to function - * -**/ - -let modules = []; - - -import defaultExport from "module-name"; -import * as name from "module-name"; -import { export1 } from "module-name"; -import { export1 as alias1 } from "module-name"; -import { default as alias } from "module-name"; -import { export1, export2 } from "module-name"; -import { export1, export2 as alias2, /* … */ } from "module-name"; -import { "string name" as alias } from "module-name"; -import defaultExport, { export1, /* … */ } from "module-name"; -import defaultExport, * as name from "module-name"; -import "module-name"; -import { "a-b" as a } from "/modules/my-module.ts"; -import myDefault from "/modules/my-module.ts"; -import myDefault, * as myModule from "/modules/my-module.ts"; diff --git a/apps/cli-go/pkg/function/testdata/nested/deno.json b/apps/cli-go/pkg/function/testdata/nested/deno.json deleted file mode 100644 index bfba6fbb2c..0000000000 --- a/apps/cli-go/pkg/function/testdata/nested/deno.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "imports": { - "module": "jsr:@supabase/functions-js/edge-runtime.d.ts" - } -} diff --git a/apps/cli-go/pkg/function/testdata/nested/index.ts b/apps/cli-go/pkg/function/testdata/nested/index.ts deleted file mode 100644 index b0539a467f..0000000000 --- a/apps/cli-go/pkg/function/testdata/nested/index.ts +++ /dev/null @@ -1 +0,0 @@ -import "module"; diff --git a/apps/cli-go/pkg/function/testdata/shared/whatever.ts b/apps/cli-go/pkg/function/testdata/shared/whatever.ts deleted file mode 100644 index 879e79cb31..0000000000 --- a/apps/cli-go/pkg/function/testdata/shared/whatever.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as _ from "../nested/index.ts"; - -export * from "./mod.ts"; diff --git a/apps/cli-go/pkg/function/testdata/types/database.ts b/apps/cli-go/pkg/function/testdata/types/database.ts deleted file mode 100644 index 4c49d8e2d0..0000000000 --- a/apps/cli-go/pkg/function/testdata/types/database.ts +++ /dev/null @@ -1,11 +0,0 @@ -export type Database = { - public: { - Tables: { - users: { - Row: { id: string; name: string } - } - } - } -} - -export type Json = string | number | boolean | null | { [key: string]: Json } | Json[] diff --git a/apps/cli-go/pkg/function/testdata/writes_import_map.form b/apps/cli-go/pkg/function/testdata/writes_import_map.form deleted file mode 100644 index 174a55f6b0..0000000000 --- a/apps/cli-go/pkg/function/testdata/writes_import_map.form +++ /dev/null @@ -1,25 +0,0 @@ ---test -Content-Disposition: form-data; name="metadata" - -{"entrypoint_path":"testdata/nested/index.ts","import_map_path":"testdata/nested/deno.json","name":"nested","static_patterns":["testdata/*/*.js"],"verify_jwt":true} - ---test -Content-Disposition: form-data; name="file"; filename="testdata/nested/deno.json" -Content-Type: application/octet-stream - -{ - "imports": { - "module": "jsr:@supabase/functions-js/edge-runtime.d.ts" - } -} - ---test -Content-Disposition: form-data; name="file"; filename="testdata/geometries/Geometries.js" -Content-Type: application/octet-stream - - ---test -Content-Disposition: form-data; name="file"; filename="testdata/nested/index.ts" -Content-Type: application/octet-stream - -import "module"; diff --git a/apps/cli-go/pkg/go.mod b/apps/cli-go/pkg/go.mod deleted file mode 100644 index 44a3f2acfd..0000000000 --- a/apps/cli-go/pkg/go.mod +++ /dev/null @@ -1,55 +0,0 @@ -module github.com/supabase/cli/pkg - -go 1.26.0 - -require ( - github.com/BurntSushi/toml v1.6.0 - github.com/andybalholm/brotli v1.2.3 - github.com/cenkalti/backoff/v4 v4.3.0 - github.com/docker/go-units v0.5.0 - github.com/ecies/go/v2 v2.0.11 - github.com/go-errors/errors v1.5.1 - github.com/go-viper/mapstructure/v2 v2.5.0 - github.com/golang-jwt/jwt/v5 v5.3.1 - github.com/h2non/gock v1.2.0 - github.com/jackc/pgconn v1.14.3 - github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 - github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 - github.com/jackc/pgproto3/v2 v2.3.3 - github.com/jackc/pgtype v1.14.4 - github.com/jackc/pgx/v4 v4.18.3 - github.com/joho/godotenv v1.5.1 - github.com/oapi-codegen/nullable v1.2.0 - github.com/oapi-codegen/runtime v1.7.0 - github.com/spf13/afero v1.15.0 - github.com/spf13/viper v1.21.0 - github.com/stretchr/testify v1.12.1 - github.com/tidwall/jsonc v0.3.3 - golang.org/x/mod v0.41.0 - google.golang.org/grpc v1.83.2 -) - -require ( - github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect - github.com/ethereum/go-ethereum v1.17.0 // indirect - github.com/fsnotify/fsnotify v1.10.1 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect - github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgio v1.0.0 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/lib/pq v1.10.9 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/sagikazarmark/locafero v0.11.0 // indirect - github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect - github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/pflag v1.0.10 // indirect - github.com/stretchr/objx v0.5.3 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - go.yaml.in/yaml/v3 v3.0.5 // indirect - golang.org/x/crypto v0.55.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/text v0.41.0 // indirect -) diff --git a/apps/cli-go/pkg/go.sum b/apps/cli-go/pkg/go.sum deleted file mode 100644 index b5fa53c40b..0000000000 --- a/apps/cli-go/pkg/go.sum +++ /dev/null @@ -1,298 +0,0 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= -github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/andybalholm/brotli v1.2.3 h1:8H1qwOkl2LPfjf3YezB90JnCliZb6SInJ/OJkEbA5NQ= -github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= -github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= -github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= -github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/ecies/go/v2 v2.0.11 h1:xYhtMdLiqNi02oLirFmLyNbVXw6250h3WM6zJryQdiM= -github.com/ecies/go/v2 v2.0.11/go.mod h1:LPRzoefP0Tam+1uesQOq3Gtb6M2OwlFUnXBTtBAKfDQ= -github.com/ethereum/go-ethereum v1.17.0 h1:2D+1Fe23CwZ5tQoAS5DfwKFNI1HGcTwi65/kRlAVxes= -github.com/ethereum/go-ethereum v1.17.0/go.mod h1:2W3msvdosS/MCWytpqTcqgFiRYbTH59FxDJzqah120o= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= -github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= -github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= -github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= -github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= -github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= -github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= -github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= -github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= -github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= -github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= -github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= -github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= -github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8= -github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= -github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= -github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= -github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= -github.com/oapi-codegen/nullable v1.2.0 h1:VflFkDW980KhBPiFF7nWSyjg+r4Obqj8lXipV0UkP5w= -github.com/oapi-codegen/nullable v1.2.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= -github.com/oapi-codegen/runtime v1.7.0 h1:t7358VYPvNbWJ9gdAkIK/smVeHpBf6yp8VTsaZsb/7k= -github.com/oapi-codegen/runtime v1.7.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= -github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= -github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= -github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= -github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= -github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= -github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= -github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= -github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= -github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tidwall/jsonc v0.3.3 h1:RVQqL3xFfDkKKXIDsrBiVQiEpBtxoKbmMXONb2H/y2w= -github.com/tidwall/jsonc v0.3.3/go.mod h1:dw+3CIxqHi+t8eFSpzzMlcVYxKp08UP5CD8/uSFCyJE= -github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= -github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= -go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= -golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= -golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= -google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/apps/cli-go/pkg/migration/apply.go b/apps/cli-go/pkg/migration/apply.go deleted file mode 100644 index e40b58be2e..0000000000 --- a/apps/cli-go/pkg/migration/apply.go +++ /dev/null @@ -1,77 +0,0 @@ -package migration - -import ( - "context" - "fmt" - "io/fs" - "os" - "path/filepath" - - "github.com/go-errors/errors" - "github.com/jackc/pgx/v4" -) - -var ( - ErrMissingRemote = errors.New("Found local migration files to be inserted before the last migration on remote database.") - ErrMissingLocal = errors.New("Remote migration versions not found in local migrations directory.") -) - -// Find unapplied local migrations older than the latest migration on -// remote, and remote migrations that are missing from local. -func FindPendingMigrations(localMigrations, remoteMigrations []string) ([]string, error) { - var unapplied, missing []string - i, j := 0, 0 - for i < len(remoteMigrations) && j < len(localMigrations) { - remote := remoteMigrations[i] - filename := filepath.Base(localMigrations[j]) - // Check if migration has been applied before, LoadLocalMigrations guarantees a match - local := migrateFilePattern.FindStringSubmatch(filename)[1] - if remote == local { - j++ - i++ - } else if remote < local { - missing = append(missing, remote) - i++ - } else { - // Include out-of-order local migrations - unapplied = append(unapplied, localMigrations[j]) - j++ - } - } - // Ensure all remote versions exist on local - if j == len(localMigrations) { - missing = append(missing, remoteMigrations[i:]...) - } - if len(missing) > 0 { - return missing, errors.New(ErrMissingLocal) - } - // Enforce migrations are applied in chronological order by default - if len(unapplied) > 0 { - return unapplied, errors.New(ErrMissingRemote) - } - pending := localMigrations[len(remoteMigrations):] - return pending, nil -} - -func ApplyMigrations(ctx context.Context, pending []string, conn *pgx.Conn, fsys fs.FS) error { - if len(pending) > 0 { - if err := CreateMigrationTable(ctx, conn); err != nil { - return err - } - } - for _, path := range pending { - filename := filepath.Base(path) - fmt.Fprintf(os.Stderr, "Applying migration %s...\n", filename) - // Reset all connection settings that might have been modified by another statement on the same connection - // eg: `SELECT pg_catalog.set_config('search_path', '', false);` - if _, err := conn.Exec(ctx, "RESET ALL"); err != nil { - return errors.Errorf("failed to reset connection state: %v", err) - } - if migration, err := NewMigrationFromFile(path, fsys); err != nil { - return err - } else if err := migration.ExecBatch(ctx, conn); err != nil { - return err - } - } - return nil -} diff --git a/apps/cli-go/pkg/migration/apply_test.go b/apps/cli-go/pkg/migration/apply_test.go deleted file mode 100644 index e6df97721f..0000000000 --- a/apps/cli-go/pkg/migration/apply_test.go +++ /dev/null @@ -1,200 +0,0 @@ -package migration - -import ( - "context" - "embed" - "os" - "testing" - fs "testing/fstest" - - "github.com/jackc/pgerrcode" - "github.com/stretchr/testify/assert" - "github.com/supabase/cli/pkg/pgtest" -) - -func TestPendingMigrations(t *testing.T) { - t.Run("finds pending migrations", func(t *testing.T) { - local := []string{ - "20221201000000_test.sql", - "20221201000001_test.sql", - "20221201000002_test.sql", - "20221201000003_test.sql", - } - remote := []string{ - "20221201000000", - "20221201000001", - } - // Run test - pending, err := FindPendingMigrations(local, remote) - // Check error - assert.NoError(t, err) - assert.ElementsMatch(t, local[2:], pending) - }) - - t.Run("throws error on missing local migration", func(t *testing.T) { - local := []string{} - remote := []string{"0"} - // Run test - pending, err := FindPendingMigrations(local, remote) - // Check error - assert.ErrorIs(t, err, ErrMissingLocal) - assert.ElementsMatch(t, remote, pending) - }) - - t.Run("throws error on missing remote version", func(t *testing.T) { - local := []string{ - "0_test.sql", - "1_test.sql", - } - remote := []string{"1"} - // Run test - pending, err := FindPendingMigrations(local, remote) - // Check error - assert.ErrorIs(t, err, ErrMissingRemote) - assert.ElementsMatch(t, local[:1], pending) - }) - - t.Run("throws error on out-of-order remote migrations", func(t *testing.T) { - local := []string{ - "20221201000000_test.sql", - "20221201000001_test.sql", - "20221201000002_test.sql", - "20221201000003_test.sql", - "20221201000004_test.sql", - } - remote := []string{ - "20221201000002", - "20221201000004", - } - // Run test - missing, err := FindPendingMigrations(local, remote) - // Check error - assert.ErrorIs(t, err, ErrMissingRemote) - assert.ElementsMatch(t, []string{local[0], local[1], local[3]}, missing) - }) - - t.Run("throws error on out-of-order local migrations", func(t *testing.T) { - local := []string{ - "20221201000000_test.sql", - "20221201000002_test.sql", - } - remote := []string{ - "20221201000000", - "20221201000001", - "20221201000002", - "20221201000003", - "20221201000004", - } - // Run test - missing, err := FindPendingMigrations(local, remote) - // Check error - assert.ErrorIs(t, err, ErrMissingLocal) - assert.ElementsMatch(t, []string{remote[1], remote[3], remote[4]}, missing) - }) -} - -var ( - //go:embed testdata - testMigrations embed.FS - //go:embed testdata/0_schema.sql - testSchema string -) - -func TestApplyMigrations(t *testing.T) { - pending := []string{"testdata/0_schema.sql"} - - t.Run("applies migrations and appends history", func(t *testing.T) { - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - mockMigrationHistory(conn). - Query("RESET ALL"). - Reply("RESET"). - Query(testSchema). - Reply("CREATE SCHEMA"). - Query(INSERT_MIGRATION_VERSION, "0", "schema", []string{testSchema}). - Reply("INSERT 0 1") - // Run test - err := ApplyMigrations(context.Background(), pending, conn.MockClient(t), testMigrations) - // Check error - assert.NoError(t, err) - }) - - t.Run("throws error on history failure", func(t *testing.T) { - // Setup in-memory fs - fsys := fs.MapFS{} - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(SET_LOCK_TIMEOUT). - Query(CREATE_VERSION_SCHEMA). - Reply("CREATE SCHEMA"). - Query(CREATE_VERSION_TABLE). - ReplyError(pgerrcode.InsufficientPrivilege, "permission denied for relation supabase_migrations"). - Query(ADD_STATEMENTS_COLUMN). - Query(ADD_NAME_COLUMN) - // Run test - err := ApplyMigrations(context.Background(), pending, conn.MockClient(t), fsys) - // Check error - assert.ErrorContains(t, err, "ERROR: permission denied for relation supabase_migrations (SQLSTATE 42501)") - }) - - t.Run("throws error on missing file", func(t *testing.T) { - // Setup in-memory fs - fsys := fs.MapFS{} - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - mockMigrationHistory(conn). - Query("RESET ALL"). - Reply("RESET") - // Run test - err := ApplyMigrations(context.Background(), pending, conn.MockClient(t), fsys) - // Check error - assert.ErrorIs(t, err, os.ErrNotExist) - }) - - t.Run("throws error on missing table", func(t *testing.T) { - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - mockMigrationHistory(conn). - Query("RESET ALL"). - Reply("RESET"). - Query(testSchema). - ReplyError(pgerrcode.UndefinedTable, `relation "supabase_migrations.schema_migrations" does not exist`). - Query(INSERT_MIGRATION_VERSION, "0", "schema", []string{testSchema}). - Reply("INSERT 0 1") - // Run test - err := ApplyMigrations(context.Background(), pending, conn.MockClient(t), testMigrations) - // Check error - assert.ErrorContains(t, err, `ERROR: relation "supabase_migrations.schema_migrations" does not exist (SQLSTATE 42P01)`) - }) - - t.Run("throws error when RESET ALL fails", func(t *testing.T) { - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - mockMigrationHistory(conn). - Query("RESET ALL"). - ReplyError(pgerrcode.InsufficientPrivilege, "permission denied for RESET ALL") - // Run test - err := ApplyMigrations(context.Background(), pending, conn.MockClient(t), testMigrations) - // Check error - assert.ErrorContains(t, err, "failed to reset connection state") - assert.ErrorContains(t, err, "ERROR: permission denied for RESET ALL (SQLSTATE 42501)") - }) -} - -func mockMigrationHistory(conn *pgtest.MockConn) *pgtest.MockConn { - conn.Query(SET_LOCK_TIMEOUT). - Query(CREATE_VERSION_SCHEMA). - Reply("CREATE SCHEMA"). - Query(CREATE_VERSION_TABLE). - Reply("CREATE TABLE"). - Query(ADD_STATEMENTS_COLUMN). - Reply("ALTER TABLE"). - Query(ADD_NAME_COLUMN). - Reply("ALTER TABLE") - return conn -} diff --git a/apps/cli-go/pkg/migration/drop.go b/apps/cli-go/pkg/migration/drop.go deleted file mode 100644 index aa44407ca1..0000000000 --- a/apps/cli-go/pkg/migration/drop.go +++ /dev/null @@ -1,50 +0,0 @@ -package migration - -import ( - "context" - _ "embed" - - "github.com/go-errors/errors" - "github.com/jackc/pgx/v4" - "github.com/supabase/cli/pkg/pgxv5" -) - -var ( - //go:embed queries/drop.sql - DropObjects string - //go:embed queries/list.sql - ListSchemas string - - // Initialised by postgres image and owned by postgres role - ManagedSchemas = []string{ - `information\_schema`, - `pg\_%`, - `\_analytics`, - `\_realtime`, - `\_supavisor`, - "pgbouncer", - "pgmq", - "pgsodium", - "pgtle", - `supabase\_migrations`, - "vault", - } -) - -func DropUserSchemas(ctx context.Context, conn *pgx.Conn) error { - migration := MigrationFile{} - migration.Statements = append(migration.Statements, DropObjects) - return migration.ExecBatch(ctx, conn) -} - -func ListUserSchemas(ctx context.Context, conn *pgx.Conn, exclude ...string) ([]string, error) { - if len(exclude) == 0 { - exclude = ManagedSchemas - } - rows, err := conn.Query(ctx, ListSchemas, exclude) - if err != nil { - return nil, errors.Errorf("failed to list schemas: %w", err) - } - // TODO: show detail and hint from pgconn.PgError - return pgxv5.CollectStrings(rows) -} diff --git a/apps/cli-go/pkg/migration/drop_test.go b/apps/cli-go/pkg/migration/drop_test.go deleted file mode 100644 index 644fb69be0..0000000000 --- a/apps/cli-go/pkg/migration/drop_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package migration - -import ( - "context" - "testing" - - "github.com/jackc/pgerrcode" - "github.com/stretchr/testify/assert" - "github.com/supabase/cli/pkg/pgtest" -) - -func TestDropSchemas(t *testing.T) { - t.Run("resets remote database", func(t *testing.T) { - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(DropObjects). - Reply("INSERT 0") - // Run test - err := DropUserSchemas(context.Background(), conn.MockClient(t)) - // Check error - assert.NoError(t, err) - }) - - t.Run("throws error on drop schema failure", func(t *testing.T) { - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(DropObjects). - ReplyError(pgerrcode.InsufficientPrivilege, "permission denied for relation supabase_migrations") - // Run test - err := DropUserSchemas(context.Background(), conn.MockClient(t)) - // Check error - assert.ErrorContains(t, err, "ERROR: permission denied for relation supabase_migrations (SQLSTATE 42501)") - }) -} diff --git a/apps/cli-go/pkg/migration/dump.go b/apps/cli-go/pkg/migration/dump.go deleted file mode 100644 index 0a1d6cdb3d..0000000000 --- a/apps/cli-go/pkg/migration/dump.go +++ /dev/null @@ -1,221 +0,0 @@ -package migration - -import ( - "context" - _ "embed" - "fmt" - "io" - "strings" - - "github.com/jackc/pgconn" -) - -var ( - //go:embed scripts/dump_schema.sh - dumpSchemaScript string - //go:embed scripts/dump_data.sh - dumpDataScript string - //go:embed scripts/dump_role.sh - dumpRoleScript string - - InternalSchemas = []string{ - "information_schema", - "pg_*", // Wildcard pattern follows pg_dump - // Initialised by supabase/postgres image and owned by postgres role - "_analytics", - "_realtime", - "_supavisor", - "auth", - "etl", - "extensions", - "pgbouncer", - "realtime", - "storage", - "supabase_functions", - "supabase_migrations", - // Owned by extensions - "cron", - "dbdev", - "graphql", - "graphql_public", - "net", - "pgmq", - "pgsodium", - "pgsodium_masks", - "pgtle", - "repack", - "tiger", - "tiger_data", - "timescaledb_*", - "_timescaledb_*", - "topology", - "vault", - } - // Data dump includes auth, storage, etc. for migrating to new project - excludedSchemas = []string{ - "information_schema", - "pg_*", // Wildcard pattern follows pg_dump - // Owned by extensions - // "cron", - "graphql", - "graphql_public", - // "net", - // "pgmq", - "pgsodium", - "pgsodium_masks", - "pgtle", - "repack", - "tiger", - "tiger_data", - "timescaledb_*", - "_timescaledb_*", - "topology", - "vault", - // Managed by Supabase - // "auth", - "etl", - "extensions", - "pgbouncer", - "realtime", - // "storage", - // "supabase_functions", - "supabase_migrations", - // TODO: Remove in a few version in favor of _supabase internal db - "_analytics", - "_realtime", - "_supavisor", - } - reservedRoles = []string{ - "anon", - "authenticated", - "authenticator", - "cli_login_.*", - "dashboard_user", - "pgbouncer", - "postgres", - "service_role", - "supabase_.*", - // Managed by extensions - "pgsodium_keyholder", - "pgsodium_keyiduser", - "pgsodium_keymaker", - "pgtle_admin", - } - allowedConfigs = []string{ - // Ref: https://github.com/supabase/postgres/blob/develop/ansible/files/postgresql_config/supautils.conf.j2#L10 - "pgaudit.*", - "pgrst.*", - "session_replication_role", - "statement_timeout", - "track_io_timing", - } -) - -type pgDumpOption struct { - schema []string - keepComments bool - excludeTable []string - columnInsert bool -} - -type DumpOptionFunc func(*pgDumpOption) - -func WithSchema(schema ...string) DumpOptionFunc { - return func(pdo *pgDumpOption) { - pdo.schema = schema - } -} - -func WithComments(keep bool) DumpOptionFunc { - return func(pdo *pgDumpOption) { - pdo.keepComments = keep - } -} - -func WithColumnInsert(use bool) DumpOptionFunc { - return func(pdo *pgDumpOption) { - pdo.columnInsert = use - } -} - -func WithoutTable(table ...string) DumpOptionFunc { - return func(pdo *pgDumpOption) { - pdo.excludeTable = table - } -} - -func toEnv(config pgconn.Config) []string { - return []string{ - "PGHOST=" + config.Host, - fmt.Sprintf("PGPORT=%d", config.Port), - "PGUSER=" + config.User, - "PGPASSWORD=" + config.Password, - "PGDATABASE=" + config.Database, - } -} - -type ExecFunc func(context.Context, string, []string, io.Writer) error - -func DumpSchema(ctx context.Context, config pgconn.Config, w io.Writer, exec ExecFunc, opts ...DumpOptionFunc) error { - var opt pgDumpOption - for _, apply := range opts { - apply(&opt) - } - env := toEnv(config) - if len(opt.schema) > 0 { - // Must append flag because empty string results in error - env = append(env, "EXTRA_FLAGS=--schema="+strings.Join(opt.schema, "|")) - } else { - env = append(env, "EXCLUDED_SCHEMAS="+strings.Join(InternalSchemas, "|")) - } - if !opt.keepComments { - env = append(env, "EXTRA_SED=/^--/d") - } - return exec(ctx, dumpSchemaScript, env, w) -} - -func DumpData(ctx context.Context, config pgconn.Config, w io.Writer, exec ExecFunc, opts ...DumpOptionFunc) error { - var opt pgDumpOption - for _, apply := range opts { - apply(&opt) - } - env := toEnv(config) - if len(opt.schema) > 0 { - env = append(env, "INCLUDED_SCHEMAS="+strings.Join(opt.schema, "|")) - } else { - env = append(env, "INCLUDED_SCHEMAS=*", "EXCLUDED_SCHEMAS="+strings.Join(excludedSchemas, "|")) - } - var extraFlags []string - if opt.columnInsert { - extraFlags = append(extraFlags, "--column-inserts", "--rows-per-insert 100000") - } - for _, table := range opt.excludeTable { - escaped := quoteUpperCase(table) - // Use separate flags to avoid error: too many dotted names - extraFlags = append(extraFlags, "--exclude-table "+escaped) - } - if len(extraFlags) > 0 { - env = append(env, "EXTRA_FLAGS="+strings.Join(extraFlags, " ")) - } - return exec(ctx, dumpDataScript, env, w) -} - -func quoteUpperCase(table string) string { - escaped := strings.ReplaceAll(table, ".", `"."`) - return fmt.Sprintf(`"%s"`, escaped) -} - -func DumpRole(ctx context.Context, config pgconn.Config, w io.Writer, exec ExecFunc, opts ...DumpOptionFunc) error { - var opt pgDumpOption - for _, apply := range opts { - apply(&opt) - } - env := append(toEnv(config), - "RESERVED_ROLES="+strings.Join(reservedRoles, "|"), - "ALLOWED_CONFIGS="+strings.Join(allowedConfigs, "|"), - ) - if !opt.keepComments { - env = append(env, "EXTRA_SED=/^--/d") - } - return exec(ctx, dumpRoleScript, env, w) -} diff --git a/apps/cli-go/pkg/migration/file.go b/apps/cli-go/pkg/migration/file.go deleted file mode 100644 index e74d322c78..0000000000 --- a/apps/cli-go/pkg/migration/file.go +++ /dev/null @@ -1,289 +0,0 @@ -package migration - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "fmt" - "io" - "io/fs" - "path/filepath" - "regexp" - "strings" - - "github.com/go-errors/errors" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" - "github.com/spf13/viper" - "github.com/supabase/cli/pkg/parser" -) - -type MigrationFile struct { - Version string - Name string - Statements []string -} - -var ( - migrateFilePattern = regexp.MustCompile(`^([0-9]+)_(.*)\.sql$`) - typeNamePattern = regexp.MustCompile(`type "([^"]+)" does not exist`) - createIndexPattern = regexp.MustCompile(`^CREATE\s+(UNIQUE\s+)?INDEX\s+CONCURRENTLY(\s|\z)`) - dropIndexPattern = regexp.MustCompile(`^DROP\s+INDEX\s+CONCURRENTLY(\s|\z)`) - reindexPattern = regexp.MustCompile(`^REINDEX(\s|\().*\sCONCURRENTLY(\s|\z)`) - vacuumPattern = regexp.MustCompile(`^VACUUM(\s|\(|\z)`) - alterSystemPattern = regexp.MustCompile(`^ALTER\s+SYSTEM(\s|\z)`) - clusterPattern = regexp.MustCompile(`^CLUSTER(\s|\z)`) -) - -func NewMigrationFromFile(path string, fsys fs.FS) (*MigrationFile, error) { - lines, err := parseFile(path, fsys) - if err != nil { - return nil, err - } - file := MigrationFile{Statements: lines} - // Parse version from file name - filename := filepath.Base(path) - matches := migrateFilePattern.FindStringSubmatch(filename) - if len(matches) > 2 { - file.Version = matches[1] - file.Name = matches[2] - } - return &file, nil -} - -func parseFile(path string, fsys fs.FS) ([]string, error) { - sql, err := fsys.Open(path) - if err != nil { - return nil, errors.Errorf("failed to open migration file: %w", err) - } - defer sql.Close() - // Unless explicitly specified, Use file length as max buffer size - if !viper.IsSet("SCANNER_BUFFER_SIZE") { - if fi, err := sql.Stat(); err == nil { - if size := int(fi.Size()); size > parser.MaxScannerCapacity { - parser.MaxScannerCapacity = size - } - } - } - return parser.SplitAndTrim(sql) -} - -func NewMigrationFromReader(sql io.Reader) (*MigrationFile, error) { - lines, err := parser.SplitAndTrim(sql) - if err != nil { - return nil, err - } - return &MigrationFile{Statements: lines}, nil -} - -func isPipelineIncompatible(sql string) bool { - upper := strings.ToUpper(trimLeadingSQLComments(sql)) - return createIndexPattern.MatchString(upper) || - dropIndexPattern.MatchString(upper) || - reindexPattern.MatchString(upper) || - vacuumPattern.MatchString(upper) || - alterSystemPattern.MatchString(upper) || - clusterPattern.MatchString(upper) -} - -func trimLeadingSQLComments(sql string) string { - trimmed := strings.TrimLeftFunc(sql, func(r rune) bool { - return r == '\ufeff' || r == ' ' || r == '\t' || r == '\n' || r == '\r' - }) - for { - switch { - case strings.HasPrefix(trimmed, "--"): - if idx := strings.IndexByte(trimmed, '\n'); idx >= 0 { - trimmed = strings.TrimLeft(trimmed[idx+1:], " \t\n\r") - continue - } - return "" - case strings.HasPrefix(trimmed, "/*"): - if idx := strings.Index(trimmed, "*/"); idx >= 0 { - trimmed = strings.TrimLeft(trimmed[idx+2:], " \t\n\r") - continue - } - return trimmed - default: - return strings.TrimSpace(trimmed) - } - } -} - -// ExecBatch is also reached from the shipped supabase-go sidecar via the -// remaining Go-delegated paths (e.g. remote `db push`/`db reset`'s -// apply.MigrateAndSeed) — local `db start`/`db reset` no longer delegate here -// at all: CLI-1954/CLI-1955 removed the hidden `db __db-bootstrap` seam -// (apps/cli-go/cmd/db.go) this comment used to describe, in favor of a fully -// native TypeScript container-bootstrap port. -func (m *MigrationFile) ExecBatch(ctx context.Context, conn *pgx.Conn) error { - batch := &pgconn.Batch{} - batchSize := 0 - executed := 0 - - formatError := func(err error, i int) error { - stat := INSERT_MIGRATION_VERSION - if i < len(m.Statements) { - stat = m.Statements[i] - } - var msg []string - var pgErr *pgconn.PgError - if errors.As(err, &pgErr) { - stat = markError(stat, int(pgErr.Position)) - if len(pgErr.Detail) > 0 { - msg = append(msg, pgErr.Detail) - } - if typeName := extractTypeName(pgErr.Message); len(typeName) > 0 && pgErr.Code == "42704" && !IsSchemaQualified(typeName) { - msg = append(msg, "") - msg = append(msg, "Hint: This type may be defined in a schema that's not in your search_path.") - msg = append(msg, " Use schema-qualified type references to avoid this error:") - msg = append(msg, fmt.Sprintf(" CREATE TABLE example (col extensions.%s);", typeName)) - msg = append(msg, " Learn more: supabase migration new --help") - } - } - msg = append(msg, fmt.Sprintf("At statement: %d", i), stat) - return errors.Errorf("%w\n%s", err, strings.Join(msg, "\n")) - } - - flushBatch := func() error { - if batchSize == 0 { - return nil - } - if result, err := conn.PgConn().ExecBatch(ctx, batch).ReadAll(); err != nil { - return formatError(err, executed+len(result)) - } - executed += batchSize - batch = &pgconn.Batch{} - batchSize = 0 - return nil - } - - for _, line := range m.Statements { - if isPipelineIncompatible(line) { - if err := flushBatch(); err != nil { - return err - } - if _, err := conn.PgConn().Exec(ctx, line).ReadAll(); err != nil { - return formatError(err, executed) - } - executed++ - } else { - batch.ExecParams(line, nil, nil, nil, nil) - batchSize++ - } - } - - // Insert into migration history - if len(m.Version) > 0 { - if err := m.insertVersionSQL(conn, batch); err != nil { - return err - } - batchSize++ - } - - return flushBatch() -} - -func markError(stat string, pos int) string { - lines := strings.Split(stat, "\n") - for j, r := range lines { - if c := len(r); pos > c { - pos -= c + 1 - continue - } - // Show a caret below the error position - if pos > 0 { - caret := append(bytes.Repeat([]byte{' '}, pos-1), '^') - lines = append(lines[:j+1], string(caret)) - } - break - } - return strings.Join(lines, "\n") -} - -// extractTypeName extracts the type name from PostgreSQL error messages like: -// 'type "ltree" does not exist' -> "ltree" -func extractTypeName(errMsg string) string { - matches := typeNamePattern.FindStringSubmatch(errMsg) - if len(matches) > 1 { - return matches[1] - } - return "" -} - -// IsSchemaQualified checks if a type name already contains a schema qualifier (e.g., "extensions.ltree") -func IsSchemaQualified(typeName string) bool { - return strings.Contains(typeName, ".") -} - -func (m *MigrationFile) insertVersionSQL(conn *pgx.Conn, batch *pgconn.Batch) error { - value := pgtype.TextArray{} - if err := value.Set(m.Statements); err != nil { - return errors.Errorf("failed to set text array: %w", err) - } - ci := conn.ConnInfo() - var err error - var encoded []byte - var valueFormat int16 - if conn.Config().PreferSimpleProtocol { - encoded, err = value.EncodeText(ci, encoded) - valueFormat = pgtype.TextFormatCode - } else { - encoded, err = value.EncodeBinary(ci, encoded) - valueFormat = pgtype.BinaryFormatCode - } - if err != nil { - return errors.Errorf("failed to encode binary: %w", err) - } - batch.ExecParams( - INSERT_MIGRATION_VERSION, - [][]byte{[]byte(m.Version), []byte(m.Name), encoded}, - []uint32{pgtype.TextOID, pgtype.TextOID, pgtype.TextArrayOID}, - []int16{pgtype.TextFormatCode, pgtype.TextFormatCode, valueFormat}, - nil, - ) - return nil -} - -type SeedFile struct { - Path string - Hash string - Dirty bool `db:"-"` -} - -func NewSeedFile(path string, fsys fs.FS) (*SeedFile, error) { - sql, err := fsys.Open(path) - if err != nil { - return nil, errors.Errorf("failed to open seed file: %w", err) - } - defer sql.Close() - hash := sha256.New() - if _, err := io.Copy(hash, sql); err != nil { - return nil, errors.Errorf("failed to hash file: %w", err) - } - digest := hex.EncodeToString(hash.Sum(nil)) - return &SeedFile{Path: path, Hash: digest}, nil -} - -func (m *SeedFile) ExecBatchWithCache(ctx context.Context, conn *pgx.Conn, fsys fs.FS) error { - // Parse each file individually to reduce memory usage - lines, err := parseFile(m.Path, fsys) - if err != nil { - return err - } - // Data statements don't mutate schemas, safe to use statement cache - batch := pgx.Batch{} - if !m.Dirty { - for _, line := range lines { - batch.Queue(line) - } - } - batch.Queue(UPSERT_SEED_FILE, m.Path, m.Hash) - // No need to track version here because there are no schema changes - if err := conn.SendBatch(ctx, &batch).Close(); err != nil { - return errors.Errorf("failed to send batch: %w", err) - } - return nil -} diff --git a/apps/cli-go/pkg/migration/file_test.go b/apps/cli-go/pkg/migration/file_test.go deleted file mode 100644 index 83087ba0e6..0000000000 --- a/apps/cli-go/pkg/migration/file_test.go +++ /dev/null @@ -1,322 +0,0 @@ -package migration - -import ( - "bufio" - "context" - "strings" - "testing" - fs "testing/fstest" - - "github.com/jackc/pgerrcode" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" - "github.com/supabase/cli/pkg/parser" - "github.com/supabase/cli/pkg/pgtest" -) - -func TestMigrationFile(t *testing.T) { - t.Run("new from file sets max token", func(t *testing.T) { - // Setup in-memory fs - path := "20220727064247_create_table.sql" - query := "BEGIN; " + strings.Repeat("a", parser.MaxScannerCapacity) - fsys := fs.MapFS{ - path: &fs.MapFile{Data: []byte(query)}, - } - // Run test - migration, err := NewMigrationFromFile(path, fsys) - // Check error - assert.NoError(t, err) - assert.Len(t, migration.Statements, 2) - assert.Equal(t, "20220727064247", migration.Version) - }) - - t.Run("new from reader errors on max token", func(t *testing.T) { - viper.Reset() - sql := "\tBEGIN; " + strings.Repeat("a", parser.MaxScannerCapacity) - // Run test - migration, err := NewMigrationFromReader(strings.NewReader(sql)) - // Check error - assert.ErrorIs(t, err, bufio.ErrTooLong) - assert.ErrorContains(t, err, "After statement 1: \tBEGIN;") - assert.Nil(t, migration) - }) - - t.Run("encodes statements in binary format", func(t *testing.T) { - migration := MigrationFile{ - Statements: []string{"create schema public"}, - Version: "0", - } - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(migration.Statements[0]). - Reply("CREATE SCHEMA"). - Query(INSERT_MIGRATION_VERSION, "0", "", migration.Statements). - Reply("INSERT 0 1") - // Run test - err := migration.ExecBatch(context.Background(), conn.MockClient(t)) - // Check error - assert.NoError(t, err) - }) - - t.Run("executes pipeline incompatible statements outside batch", func(t *testing.T) { - migration := MigrationFile{ - Statements: []string{ - "create table public.widgets(id bigint primary key)", - "CREATE UNIQUE INDEX CONCURRENTLY widgets_id_idx ON public.widgets(id)", - "alter table public.widgets enable row level security", - }, - Version: "20260101000000", - Name: "create_widgets", - } - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(migration.Statements[0]). - Reply("CREATE TABLE"). - SimpleQuery(migration.Statements[1]). - Reply("CREATE INDEX"). - Query(migration.Statements[2]). - Reply("ALTER TABLE"). - Query(INSERT_MIGRATION_VERSION, migration.Version, migration.Name, migration.Statements). - Reply("INSERT 0 1") - // Run test - err := migration.ExecBatch(context.Background(), conn.MockClient(t)) - // Check error - assert.NoError(t, err) - }) - - t.Run("records migration version when file has no statements", func(t *testing.T) { - migration := MigrationFile{ - Version: "20260101000000", - Name: "empty_migration", - } - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(INSERT_MIGRATION_VERSION, migration.Version, migration.Name, migration.Statements). - Reply("INSERT 0 1") - // Run test - err := migration.ExecBatch(context.Background(), conn.MockClient(t)) - // Check error - assert.NoError(t, err) - }) - - t.Run("reports pipeline incompatible statement errors with statement index", func(t *testing.T) { - migration := MigrationFile{ - Statements: []string{ - "create table public.widgets(id bigint primary key)", - "CREATE INDEX CONCURRENTLY widgets_id_idx ON public.widgets(id)", - "alter table public.widgets enable row level security", - }, - Version: "20260101000000", - Name: "create_widgets", - } - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(migration.Statements[0]). - Reply("CREATE TABLE"). - SimpleQuery(migration.Statements[1]). - ReplyError("25001", "CREATE INDEX CONCURRENTLY cannot be executed within a pipeline") - // Run test - err := migration.ExecBatch(context.Background(), conn.MockClient(t)) - // Check error - assert.ErrorContains(t, err, "ERROR: CREATE INDEX CONCURRENTLY cannot be executed within a pipeline (SQLSTATE 25001)") - assert.ErrorContains(t, err, "At statement: 1\nCREATE INDEX CONCURRENTLY widgets_id_idx ON public.widgets(id)") - }) - - t.Run("throws error on insert failure", func(t *testing.T) { - migration := MigrationFile{ - Statements: []string{"create schema public"}, - Version: "0", - } - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(migration.Statements[0]). - ReplyError(pgerrcode.DuplicateSchema, `schema "public" already exists`). - Query(INSERT_MIGRATION_VERSION, "0", "", migration.Statements). - Reply("INSERT 0 1") - // Run test - err := migration.ExecBatch(context.Background(), conn.MockClient(t)) - // Check error - assert.ErrorContains(t, err, "ERROR: schema \"public\" already exists (SQLSTATE 42P06)") - assert.ErrorContains(t, err, "At statement: 0\ncreate schema public") - }) - - t.Run("provides helpful hint for extension type errors", func(t *testing.T) { - migration := MigrationFile{ - Statements: []string{"CREATE TABLE test (path ltree NOT NULL)"}, - Version: "0", - } - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(migration.Statements[0]). - ReplyError("42704", `type "ltree" does not exist`). - Query(INSERT_MIGRATION_VERSION, "0", "", migration.Statements). - Reply("INSERT 0 1") - // Run test - err := migration.ExecBatch(context.Background(), conn.MockClient(t)) - // Check error - assert.ErrorContains(t, err, `type "ltree" does not exist`) - assert.ErrorContains(t, err, "Hint: This type may be defined in a schema") - assert.ErrorContains(t, err, "extensions.ltree") - assert.ErrorContains(t, err, "supabase migration new --help") - assert.ErrorContains(t, err, "At statement: 0") - }) - - t.Run("skips hint for schema-qualified type errors", func(t *testing.T) { - migration := MigrationFile{ - Statements: []string{"CREATE TABLE test (path extensions.ltree NOT NULL)"}, - Version: "0", - } - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(migration.Statements[0]). - ReplyError("42704", `type "extensions.ltree" does not exist`). - Query(INSERT_MIGRATION_VERSION, "0", "", migration.Statements). - Reply("INSERT 0 1") - // Run test - err := migration.ExecBatch(context.Background(), conn.MockClient(t)) - // Check error - should NOT contain hint since type is already schema-qualified - assert.ErrorContains(t, err, `type "extensions.ltree" does not exist`) - assert.NotContains(t, err.Error(), "Hint: This type may be defined in a schema") - }) -} - -func TestExtractTypeName(t *testing.T) { - t.Run("extracts type name from standard error message", func(t *testing.T) { - result := extractTypeName(`type "ltree" does not exist`) - assert.Equal(t, "ltree", result) - }) - - t.Run("extracts schema-qualified type name", func(t *testing.T) { - result := extractTypeName(`type "extensions.ltree" does not exist`) - assert.Equal(t, "extensions.ltree", result) - }) - - t.Run("extracts type with underscores", func(t *testing.T) { - result := extractTypeName(`type "my_custom_type" does not exist`) - assert.Equal(t, "my_custom_type", result) - }) - - t.Run("returns empty string for non-matching message", func(t *testing.T) { - result := extractTypeName(`column "name" does not exist`) - assert.Equal(t, "", result) - }) - - t.Run("returns empty string for empty message", func(t *testing.T) { - result := extractTypeName("") - assert.Equal(t, "", result) - }) - - t.Run("handles type names with numbers", func(t *testing.T) { - result := extractTypeName(`type "type123" does not exist`) - assert.Equal(t, "type123", result) - }) -} - -func TestIsPipelineIncompatible(t *testing.T) { - cases := []struct { - name string - sql string - want bool - }{ - { - name: "create index concurrently", - sql: "CREATE INDEX CONCURRENTLY widgets_id_idx ON public.widgets(id)", - want: true, - }, - { - name: "create unique index concurrently", - sql: "CREATE UNIQUE INDEX CONCURRENTLY widgets_id_idx ON public.widgets(id)", - want: true, - }, - { - name: "create index concurrently after comments", - sql: "-- cannot run in a transaction\n/* generated */\nCREATE INDEX CONCURRENTLY widgets_id_idx ON public.widgets(id)", - want: true, - }, - { - name: "drop index concurrently", - sql: "DROP INDEX CONCURRENTLY public.widgets_id_idx", - want: true, - }, - { - name: "drop index concurrently if exists", - sql: "drop index concurrently if exists api.idx_rx_orders_clinic_id", - want: true, - }, - { - name: "reindex table concurrently", - sql: "REINDEX TABLE CONCURRENTLY public.widgets", - want: true, - }, - { - name: "reindex with options concurrently", - sql: "REINDEX (VERBOSE) INDEX CONCURRENTLY widgets_id_idx", - want: true, - }, - { - name: "vacuum bare", - sql: "VACUUM", - want: true, - }, - { - name: "vacuum with options", - sql: "VACUUM (ANALYZE) public.widgets", - want: true, - }, - { - name: "alter system", - sql: "ALTER SYSTEM SET log_statement = 'all'", - want: true, - }, - { - name: "cluster", - sql: "CLUSTER public.widgets USING widgets_id_idx", - want: true, - }, - { - name: "ordinary create index", - sql: "CREATE INDEX widgets_id_idx ON public.widgets(id)", - want: false, - }, - { - name: "ordinary drop index", - sql: "DROP INDEX IF EXISTS public.widgets_id_idx", - want: false, - }, - { - name: "concurrently in string literal", - sql: "SELECT 'CREATE INDEX CONCURRENTLY widgets_id_idx ON public.widgets(id)'", - want: false, - }, - { - name: "concurrently in leading comment only", - sql: "-- CREATE INDEX CONCURRENTLY widgets_id_idx ON public.widgets(id)\nSELECT 1", - want: false, - }, - { - name: "word prefix", - sql: "VACUUMING public.widgets", - want: false, - }, - } - - for _, tt := range cases { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, isPipelineIncompatible(tt.sql)) - }) - } -} - -func TestIsSchemaQualified(t *testing.T) { - assert.True(t, IsSchemaQualified("extensions.ltree")) - assert.True(t, IsSchemaQualified("public.my_type")) - assert.False(t, IsSchemaQualified("ltree")) - assert.False(t, IsSchemaQualified("")) -} diff --git a/apps/cli-go/pkg/migration/history.go b/apps/cli-go/pkg/migration/history.go deleted file mode 100644 index 9f156faa4a..0000000000 --- a/apps/cli-go/pkg/migration/history.go +++ /dev/null @@ -1,73 +0,0 @@ -package migration - -import ( - "context" - - "github.com/go-errors/errors" - "github.com/jackc/pgconn" - "github.com/jackc/pgx/v4" - "github.com/supabase/cli/pkg/pgxv5" -) - -const ( - SET_LOCK_TIMEOUT = "SET lock_timeout = '4s'" - CREATE_VERSION_SCHEMA = "CREATE SCHEMA IF NOT EXISTS supabase_migrations" - CREATE_VERSION_TABLE = "CREATE TABLE IF NOT EXISTS supabase_migrations.schema_migrations (version text NOT NULL PRIMARY KEY)" - ADD_STATEMENTS_COLUMN = "ALTER TABLE supabase_migrations.schema_migrations ADD COLUMN IF NOT EXISTS statements text[]" - ADD_NAME_COLUMN = "ALTER TABLE supabase_migrations.schema_migrations ADD COLUMN IF NOT EXISTS name text" - INSERT_MIGRATION_VERSION = "INSERT INTO supabase_migrations.schema_migrations(version, name, statements) VALUES($1, $2, $3)" - UPSERT_MIGRATION_VERSION = INSERT_MIGRATION_VERSION + " ON CONFLICT (version) DO UPDATE SET name = EXCLUDED.name, statements = EXCLUDED.statements" - DELETE_MIGRATION_VERSION = "DELETE FROM supabase_migrations.schema_migrations WHERE version = ANY($1)" - DELETE_MIGRATION_BEFORE = "DELETE FROM supabase_migrations.schema_migrations WHERE version <= $1" - TRUNCATE_VERSION_TABLE = "TRUNCATE supabase_migrations.schema_migrations" - SELECT_VERSION_TABLE = "SELECT version, coalesce(name, '') as name, statements FROM supabase_migrations.schema_migrations" - LIST_MIGRATION_VERSION = "SELECT version FROM supabase_migrations.schema_migrations ORDER BY version" - CREATE_SEED_TABLE = "CREATE TABLE IF NOT EXISTS supabase_migrations.seed_files (path text NOT NULL PRIMARY KEY, hash text NOT NULL)" - UPSERT_SEED_FILE = "INSERT INTO supabase_migrations.seed_files(path, hash) VALUES($1, $2) ON CONFLICT (path) DO UPDATE SET hash = EXCLUDED.hash" - SELECT_SEED_TABLE = "SELECT path, hash FROM supabase_migrations.seed_files" -) - -// TODO: support overriding `supabase_migrations.schema_migrations` with user defined . -func CreateMigrationTable(ctx context.Context, conn *pgx.Conn) error { - // This must be run without prepared statements because each statement in the batch depends on - // the previous schema change. The lock timeout will be reset when implicit transaction ends. - batch := pgconn.Batch{} - batch.ExecParams(SET_LOCK_TIMEOUT, nil, nil, nil, nil) - batch.ExecParams(CREATE_VERSION_SCHEMA, nil, nil, nil, nil) - batch.ExecParams(CREATE_VERSION_TABLE, nil, nil, nil, nil) - batch.ExecParams(ADD_STATEMENTS_COLUMN, nil, nil, nil, nil) - batch.ExecParams(ADD_NAME_COLUMN, nil, nil, nil, nil) - if _, err := conn.PgConn().ExecBatch(ctx, &batch).ReadAll(); err != nil { - return errors.Errorf("failed to create migration table: %w", err) - } - return nil -} - -func ReadMigrationTable(ctx context.Context, conn *pgx.Conn) ([]MigrationFile, error) { - rows, err := conn.Query(ctx, SELECT_VERSION_TABLE) - if err != nil { - return nil, errors.Errorf("failed to read migration table: %w", err) - } - return pgxv5.CollectRows[MigrationFile](rows) -} - -func CreateSeedTable(ctx context.Context, conn *pgx.Conn) error { - // This must be run without prepared statements because each statement in the batch depends on - // the previous schema change. The lock timeout will be reset when implicit transaction ends. - batch := pgconn.Batch{} - batch.ExecParams(SET_LOCK_TIMEOUT, nil, nil, nil, nil) - batch.ExecParams(CREATE_VERSION_SCHEMA, nil, nil, nil, nil) - batch.ExecParams(CREATE_SEED_TABLE, nil, nil, nil, nil) - if _, err := conn.PgConn().ExecBatch(ctx, &batch).ReadAll(); err != nil { - return errors.Errorf("failed to create seed table: %w", err) - } - return nil -} - -func ReadSeedTable(ctx context.Context, conn *pgx.Conn) ([]SeedFile, error) { - rows, err := conn.Query(ctx, SELECT_SEED_TABLE) - if err != nil { - return nil, errors.Errorf("failed to read seed table: %w", err) - } - return pgxv5.CollectRows[SeedFile](rows) -} diff --git a/apps/cli-go/pkg/migration/list.go b/apps/cli-go/pkg/migration/list.go deleted file mode 100644 index 8972bb2594..0000000000 --- a/apps/cli-go/pkg/migration/list.go +++ /dev/null @@ -1,77 +0,0 @@ -package migration - -import ( - "context" - "fmt" - "io/fs" - "os" - "path/filepath" - "regexp" - "strconv" - - "github.com/go-errors/errors" - "github.com/jackc/pgconn" - "github.com/jackc/pgerrcode" - "github.com/jackc/pgx/v4" - "github.com/supabase/cli/pkg/pgxv5" -) - -func ListRemoteMigrations(ctx context.Context, conn *pgx.Conn) ([]string, error) { - // We query the version string only for backwards compatibility - rows, _ := conn.Query(ctx, LIST_MIGRATION_VERSION) - versions, err := pgxv5.CollectStrings(rows) - if err != nil { - var pgErr *pgconn.PgError - if errors.As(err, &pgErr) && pgErr.Code == pgerrcode.UndefinedTable { - // If migration history table is undefined, the remote project has no migrations - return nil, nil - } - } - return versions, err -} - -func ListLocalMigrations(migrationsDir string, fsys fs.FS, filter ...func(string) bool) ([]string, error) { - localMigrations, err := fs.ReadDir(fsys, migrationsDir) - if err != nil && !errors.Is(err, os.ErrNotExist) { - return nil, errors.Errorf("failed to read directory: %w", err) - } - var clean []string -OUTER: - for i, migration := range localMigrations { - if migration.IsDir() { - continue - } - filename := migration.Name() - if i == 0 && shouldSkip(filename) { - fmt.Fprintf(os.Stderr, "Skipping migration %s... (replace \"init\" with a different file name to apply this migration)\n", filename) - continue - } - matches := migrateFilePattern.FindStringSubmatch(filename) - if len(matches) == 0 { - fmt.Fprintf(os.Stderr, "Skipping migration %s... (file name must match pattern \"_name.sql\")\n", filename) - continue - } - path := filepath.Join(migrationsDir, filename) - for _, keep := range filter { - if version := matches[1]; !keep(version) { - continue OUTER - } - } - clean = append(clean, path) - } - return clean, nil -} - -var initSchemaPattern = regexp.MustCompile(`([0-9]{14})_init\.sql`) - -func shouldSkip(name string) bool { - // NOTE: To handle backward-compatibility. `_init.sql` as - // the first migration (prev versions of the CLI) is deprecated. - matches := initSchemaPattern.FindStringSubmatch(name) - if len(matches) == 2 { - if timestamp, err := strconv.ParseUint(matches[1], 10, 64); err == nil && timestamp < 20211209000000 { - return true - } - } - return false -} diff --git a/apps/cli-go/pkg/migration/list_test.go b/apps/cli-go/pkg/migration/list_test.go deleted file mode 100644 index 4654fa876a..0000000000 --- a/apps/cli-go/pkg/migration/list_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package migration - -import ( - "context" - "testing" - fs "testing/fstest" - - "github.com/jackc/pgerrcode" - "github.com/stretchr/testify/assert" - "github.com/supabase/cli/pkg/pgtest" -) - -func TestRemoteMigrations(t *testing.T) { - t.Run("loads migration versions", func(t *testing.T) { - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(LIST_MIGRATION_VERSION). - Reply("SELECT 1", []any{"20220727064247"}) - // Run test - versions, err := ListRemoteMigrations(context.Background(), conn.MockClient(t)) - // Check error - assert.NoError(t, err) - assert.ElementsMatch(t, []string{"20220727064247"}, versions) - }) - - t.Run("loads empty migrations on missing table", func(t *testing.T) { - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(LIST_MIGRATION_VERSION). - ReplyError(pgerrcode.UndefinedTable, "relation \"supabase_migrations.schema_migrations\" does not exist") - // Run test - versions, err := ListRemoteMigrations(context.Background(), conn.MockClient(t)) - // Check error - assert.NoError(t, err) - assert.Empty(t, versions) - }) - - t.Run("throws error on invalid row", func(t *testing.T) { - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(LIST_MIGRATION_VERSION). - Reply("SELECT 1", []any{}) - // Run test - _, err := ListRemoteMigrations(context.Background(), conn.MockClient(t)) - // Check error - assert.ErrorContains(t, err, "number of field descriptions must equal number of destinations, got 0 and 1") - }) -} - -func TestLocalMigrations(t *testing.T) { - t.Run("loads migration versions", func(t *testing.T) { - // Setup in-memory fs - files := []string{ - "20220727064246_test.sql", - "20220727064248_test.sql", - } - fsys := fs.MapFS{} - for _, name := range files { - fsys[name] = &fs.MapFile{} - } - // Run test - versions, err := ListLocalMigrations(".", fsys) - // Check error - assert.NoError(t, err) - assert.ElementsMatch(t, files, versions) - }) - - t.Run("ignores outdated and invalid files", func(t *testing.T) { - // Setup in-memory fs - fsys := fs.MapFS{ - "20211208000000_init.sql": &fs.MapFile{}, - "20211208000001_invalid.ts": &fs.MapFile{}, - } - // Run test - versions, err := ListLocalMigrations(".", fsys) - // Check error - assert.NoError(t, err) - assert.Empty(t, versions) - }) - - t.Run("throws error on open failure", func(t *testing.T) { - // Setup in-memory fs - fsys := fs.MapFS{"migrations": &fs.MapFile{}} - // Run test - _, err := ListLocalMigrations("migrations", fsys) - // Check error - assert.ErrorContains(t, err, "failed to read directory:") - }) -} diff --git a/apps/cli-go/pkg/migration/queries/drop.sql b/apps/cli-go/pkg/migration/queries/drop.sql deleted file mode 100644 index 568e15eef0..0000000000 --- a/apps/cli-go/pkg/migration/queries/drop.sql +++ /dev/null @@ -1,136 +0,0 @@ -do $$ declare - rec record; -begin - -- schemas - for rec in - select pn.* - from pg_catalog.pg_namespace pn - left join pg_catalog.pg_depend pd on pd.objid = pn.oid and pd.classid = 'pg_catalog.pg_namespace'::regclass - where pd.deptype is null - and not pn.nspname like any(array['information\_schema', 'pg\_%', '\_analytics', '\_realtime', '\_supavisor', 'pgbouncer', 'pgmq', 'pgsodium', 'pgtle', 'supabase\_migrations', 'vault', 'extensions', 'public']) - and pn.nspowner::regrole::text != 'supabase_admin' - loop - -- If an extension uses a schema it doesn't create, dropping the schema will cascade to also - -- drop the extension. But if an extension creates its own schema, dropping the schema will - -- throw an error. Hence, we drop schemas first while excluding those created by extensions. - raise notice 'dropping schema: %', rec.nspname; - execute format('drop schema if exists %I cascade', rec.nspname); - end loop; - - -- extensions - for rec in - select * - from pg_extension p - where p.extname not in ('pg_graphql', 'pg_net', 'pg_stat_statements', 'pgcrypto', 'pgjwt', 'pgsodium', 'plpgsql', 'supabase_vault', 'uuid-ossp') - loop - raise notice 'dropping extension: %', rec.extname; - execute format('drop extension if exists %I cascade', rec.extname); - end loop; - - -- functions - for rec in - select * - from pg_proc p - where p.pronamespace::regnamespace::name = 'public' - loop - -- supports aggregate, function, and procedure - raise notice 'dropping function: %.%', rec.pronamespace::regnamespace::name, rec.proname; - execute format('drop routine if exists %I.%I(%s) cascade', rec.pronamespace::regnamespace::name, rec.proname, pg_catalog.pg_get_function_identity_arguments(rec.oid)); - end loop; - - -- views (necessary for views referencing objects in Supabase-managed schemas) - for rec in - select * - from pg_class c - where - c.relnamespace::regnamespace::name = 'public' - and c.relkind = 'v' - loop - raise notice 'dropping view: %.%', rec.relnamespace::regnamespace::name, rec.relname; - execute format('drop view if exists %I.%I cascade', rec.relnamespace::regnamespace::name, rec.relname); - end loop; - - -- materialized views (necessary for materialized views referencing objects in Supabase-managed schemas) - for rec in - select * - from pg_class c - where - c.relnamespace::regnamespace::name = 'public' - and c.relkind = 'm' - loop - raise notice 'dropping materialized view: %.%', rec.relnamespace::regnamespace::name, rec.relname; - execute format('drop materialized view if exists %I.%I cascade', rec.relnamespace::regnamespace::name, rec.relname); - end loop; - - -- tables (cascade to dependent objects) - for rec in - select * - from pg_class c - where - c.relnamespace::regnamespace::name = 'public' - and c.relkind not in ('c', 'S', 'v', 'm') - order by c.relkind desc - loop - -- supports all table like relations, except views, complex types, and sequences - raise notice 'dropping table: %.%', rec.relnamespace::regnamespace::name, rec.relname; - execute format('drop table if exists %I.%I cascade', rec.relnamespace::regnamespace::name, rec.relname); - end loop; - - -- truncate tables in auth, webhooks, and migrations schema - for rec in - select * - from pg_class c - where - (c.relnamespace::regnamespace::name = 'auth' and c.relname != 'schema_migrations' - or c.relnamespace::regnamespace::name = 'supabase_functions' and c.relname != 'migrations' - or c.relnamespace::regnamespace::name = 'supabase_migrations') - and c.relkind = 'r' - loop - raise notice 'truncating table: %.%', rec.relnamespace::regnamespace::name, rec.relname; - execute format('truncate %I.%I cascade', rec.relnamespace::regnamespace::name, rec.relname); - end loop; - - -- sequences - for rec in - select * - from pg_class c - where - c.relnamespace::regnamespace::name = 'public' - and c.relkind = 's' - loop - raise notice 'dropping sequence: %.%', rec.relnamespace::regnamespace::name, rec.relname; - execute format('drop sequence if exists %I.%I cascade', rec.relnamespace::regnamespace::name, rec.relname); - end loop; - - -- types - for rec in - select * - from pg_type t - where - t.typnamespace::regnamespace::name = 'public' - and typtype != 'b' - loop - raise notice 'dropping type: %.%', rec.typnamespace::regnamespace::name, rec.typname; - execute format('drop type if exists %I.%I cascade', rec.typnamespace::regnamespace::name, rec.typname); - end loop; - - -- policies - for rec in - select * - from pg_policies p - loop - raise notice 'dropping policy: %', rec.policyname; - execute format('drop policy if exists %I on %I.%I cascade', rec.policyname, rec.schemaname, rec.tablename); - end loop; - - -- publications - for rec in - select * - from pg_publication p - where - not p.pubname like any(array['supabase\_realtime%', 'realtime\_messages%']) - loop - raise notice 'dropping publication: %', rec.pubname; - execute format('drop publication if exists %I', rec.pubname); - end loop; -end $$; diff --git a/apps/cli-go/pkg/migration/queries/list.sql b/apps/cli-go/pkg/migration/queries/list.sql deleted file mode 100644 index 7ab6201174..0000000000 --- a/apps/cli-go/pkg/migration/queries/list.sql +++ /dev/null @@ -1,10 +0,0 @@ --- List user defined schemas, excluding --- Extension created schemas --- Supabase managed schemas -select pn.nspname -from pg_catalog.pg_namespace pn -left join pg_catalog.pg_depend pd on pd.objid = pn.oid and pd.classid = 'pg_catalog.pg_namespace'::regclass -where pd.deptype is null - and not pn.nspname like any($1) - and pn.nspowner::regrole::text != 'supabase_admin' -order by pn.nspname diff --git a/apps/cli-go/pkg/migration/scripts/dump_data.sh b/apps/cli-go/pkg/migration/scripts/dump_data.sh deleted file mode 100755 index fed47b54e1..0000000000 --- a/apps/cli-go/pkg/migration/scripts/dump_data.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -export PGHOST="$PGHOST" -export PGPORT="$PGPORT" -export PGUSER="$PGUSER" -export PGPASSWORD="$PGPASSWORD" -export PGDATABASE="$PGDATABASE" - -# Disable triggers so that data dump can be restored exactly as it is -echo "SET session_replication_role = replica; -" - -# Explanation of pg_dump flags: -# -# --exclude-schema omit data from internal schemas as they are maintained by platform -# --exclude-table omit data from migration history tables as they are managed by platform -# --column-inserts only column insert syntax is supported, ie. no copy from stdin -# --schema '*' include all other schemas by default -# -# Explanation of sed substitutions: -# -# - do not emit psql meta commands -# -# Never delete SQL comments because multiline records may begin with them. -pg_dump \ - --data-only \ - --quote-all-identifier \ - --role "postgres" \ - --exclude-schema "${EXCLUDED_SCHEMAS:-}" \ - --exclude-table "auth.schema_migrations" \ - --exclude-table "storage.migrations" \ - --exclude-table "supabase_functions.migrations" \ - --schema "$INCLUDED_SCHEMAS" \ - ${EXTRA_FLAGS:-} \ -| sed -E 's/^\\(un)?restrict .*$/-- &/' - -# Reset session config generated by pg_dump -echo "RESET ALL;" diff --git a/apps/cli-go/pkg/migration/scripts/dump_role.sh b/apps/cli-go/pkg/migration/scripts/dump_role.sh deleted file mode 100755 index 96fe6a3068..0000000000 --- a/apps/cli-go/pkg/migration/scripts/dump_role.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -export PGHOST="$PGHOST" -export PGPORT="$PGPORT" -export PGUSER="$PGUSER" -export PGPASSWORD="$PGPASSWORD" -export PGDATABASE="$PGDATABASE" - -# Explanation of pg_dumpall flags: -# -# --roles-only only include create, alter, and grant role statements -# -# Explanation of sed substitutions: -# -# - do not emit psql meta commands -# - do not create or alter reserved roles as they are blocked by supautils -# - explicitly allow altering safe attributes, ie. statement_timeout, pgrst.* -# - discard role attributes that require superuser, ie. nosuperuser, noreplication -# - do not alter membership grants by supabase_admin role -pg_dumpall \ - --roles-only \ - --role "postgres" \ - --quote-all-identifier \ - --no-role-passwords \ - --no-comments \ -| sed -E 's/^\\(un)?restrict .*$/-- &/' \ -| sed -E "s/^CREATE ROLE \"($RESERVED_ROLES)\"/-- &/" \ -| sed -E "s/^ALTER ROLE \"($RESERVED_ROLES)\"/-- &/" \ -| sed -E "s/ (NOSUPERUSER|NOREPLICATION)//g" \ -| sed -E "s/^-- (.* SET \"($ALLOWED_CONFIGS)\" .*)/\1/" \ -| sed -E "s/GRANT \".*\" TO \"($RESERVED_ROLES)\"/-- &/" \ -| sed -E "${EXTRA_SED:-}" \ -| uniq - -# Reset session config generated by pg_dump -echo "RESET ALL;" diff --git a/apps/cli-go/pkg/migration/scripts/dump_schema.sh b/apps/cli-go/pkg/migration/scripts/dump_schema.sh deleted file mode 100755 index c136cc086e..0000000000 --- a/apps/cli-go/pkg/migration/scripts/dump_schema.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -export PGHOST="$PGHOST" -export PGPORT="$PGPORT" -export PGUSER="$PGUSER" -export PGPASSWORD="$PGPASSWORD" -export PGDATABASE="$PGDATABASE" - -# Explanation of pg_dump flags: -# -# --schema-only omit data like migration history, pgsodium key, etc. -# --exclude-schema omit internal schemas as they are maintained by platform -# -# Explanation of sed substitutions: -# -# - do not emit psql meta commands -# - do not alter superuser role "supabase_admin" -# - do not alter foreign data wrappers owner -# - do not include ACL changes on internal schemas -# - do not include RLS policies on cron extension schema -# - do not include event triggers -# - do not create pgtle schema and extension comments -# - do not create publication "supabase_realtime" -# - do not set transaction_timeout which requires pg17 -pg_dump \ - --schema-only \ - --quote-all-identifier \ - --role "postgres" \ - --exclude-schema "${EXCLUDED_SCHEMAS:-}" \ - ${EXTRA_FLAGS:-} \ -| sed -E 's/^\\(un)?restrict .*$/-- &/' \ -| sed -E 's/^CREATE SCHEMA "/CREATE SCHEMA IF NOT EXISTS "/' \ -| sed -E 's/^CREATE TABLE "/CREATE TABLE IF NOT EXISTS "/' \ -| sed -E 's/^CREATE SEQUENCE "/CREATE SEQUENCE IF NOT EXISTS "/' \ -| sed -E 's/^CREATE VIEW "/CREATE OR REPLACE VIEW "/' \ -| sed -E 's/^CREATE FUNCTION "/CREATE OR REPLACE FUNCTION "/' \ -| sed -E 's/^CREATE TRIGGER "/CREATE OR REPLACE TRIGGER "/' \ -| sed -E 's/^CREATE PUBLICATION "supabase_realtime/-- &/' \ -| sed -E 's/^CREATE EVENT TRIGGER /-- &/' \ -| sed -E 's/^ WHEN TAG IN /-- &/' \ -| sed -E 's/^ EXECUTE FUNCTION /-- &/' \ -| sed -E 's/^ALTER EVENT TRIGGER /-- &/' \ -| sed -E 's/^ALTER PUBLICATION "supabase_realtime_/-- &/' \ -| sed -E 's/^ALTER FOREIGN DATA WRAPPER (.+) OWNER TO /-- &/' \ -| sed -E 's/^ALTER DEFAULT PRIVILEGES FOR ROLE "supabase_admin"/-- &/' \ -| sed -E 's/^GRANT ALL ON FOREIGN DATA WRAPPER (.+) TO "postgres" WITH GRANT OPTION/-- &/' \ -| sed -E "s/^GRANT (.+) ON (.+) \"(${EXCLUDED_SCHEMAS:-})\"/-- &/" \ -| sed -E "s/^REVOKE (.+) ON (.+) \"(${EXCLUDED_SCHEMAS:-})\"/-- &/" \ -| sed -E 's/^(CREATE EXTENSION IF NOT EXISTS "pg_tle").+/\1;/' \ -| sed -E 's/^(CREATE EXTENSION IF NOT EXISTS "pgsodium").+/\1;/' \ -| sed -E 's/^(CREATE EXTENSION IF NOT EXISTS "pgmq").+/\1;/' \ -| sed -E 's/^COMMENT ON EXTENSION (.+)/-- &/' \ -| sed -E 's/^CREATE POLICY "cron_job_/-- &/' \ -| sed -E 's/^ALTER TABLE "cron"/-- &/' \ -| sed -E 's/^SET transaction_timeout = 0;/-- &/' \ -| sed -E "${EXTRA_SED:-}" diff --git a/apps/cli-go/pkg/migration/seed.go b/apps/cli-go/pkg/migration/seed.go deleted file mode 100644 index 2e8fff71c8..0000000000 --- a/apps/cli-go/pkg/migration/seed.go +++ /dev/null @@ -1,100 +0,0 @@ -package migration - -import ( - "context" - "fmt" - "io/fs" - "os" - "path/filepath" - - "github.com/go-errors/errors" - "github.com/jackc/pgconn" - "github.com/jackc/pgerrcode" - "github.com/jackc/pgx/v4" - "github.com/supabase/cli/pkg/config" -) - -func getRemoteSeeds(ctx context.Context, conn *pgx.Conn) (map[string]string, error) { - remotes, err := ReadSeedTable(ctx, conn) - if err != nil { - var pgErr *pgconn.PgError - if errors.As(err, &pgErr) && pgErr.Code == pgerrcode.UndefinedTable { - // If seed table is undefined, the remote project has no migrations - return nil, nil - } - return nil, err - } - applied := make(map[string]string, len(remotes)) - for _, seed := range remotes { - applied[seed.Path] = seed.Hash - } - return applied, nil -} - -func GetPendingSeeds(ctx context.Context, locals config.Glob, conn *pgx.Conn, fsys fs.FS) ([]SeedFile, error) { - locals, err := locals.SQLFiles(fsys) - if err != nil { - fmt.Fprintln(os.Stderr, "WARN:", err) - } - if len(locals) == 0 { - return nil, nil - } - applied, err := getRemoteSeeds(ctx, conn) - if err != nil { - return nil, err - } - var pending []SeedFile - for _, path := range locals { - seed, err := NewSeedFile(path, fsys) - if err != nil { - return nil, err - } - if hash, exists := applied[seed.Path]; exists { - // Skip seed files that already exist - if hash == seed.Hash { - continue - } - // Mark seed file as dirty - seed.Dirty = true - } - pending = append(pending, *seed) - } - return pending, nil -} - -func SeedData(ctx context.Context, pending []SeedFile, conn *pgx.Conn, fsys fs.FS) error { - if len(pending) > 0 { - if err := CreateSeedTable(ctx, conn); err != nil { - return err - } - } - for _, seed := range pending { - if seed.Dirty { - fmt.Fprintf(os.Stderr, "Updating seed hash to %s...\n", seed.Path) - } else { - fmt.Fprintf(os.Stderr, "Seeding data from %s...\n", seed.Path) - } - // Batch seed commands, safe to use statement cache - if err := seed.ExecBatchWithCache(ctx, conn, fsys); err != nil { - return err - } - } - return nil -} - -func SeedGlobals(ctx context.Context, pending []string, conn *pgx.Conn, fsys fs.FS) error { - for _, path := range pending { - filename := filepath.Base(path) - fmt.Fprintf(os.Stderr, "Seeding globals from %s...\n", filename) - globals, err := NewMigrationFromFile(path, fsys) - if err != nil { - return err - } - // Skip inserting to migration history - globals.Version = "" - if err := globals.ExecBatch(ctx, conn); err != nil { - return err - } - } - return nil -} diff --git a/apps/cli-go/pkg/migration/seed_test.go b/apps/cli-go/pkg/migration/seed_test.go deleted file mode 100644 index db4337b54c..0000000000 --- a/apps/cli-go/pkg/migration/seed_test.go +++ /dev/null @@ -1,176 +0,0 @@ -package migration - -import ( - "context" - _ "embed" - "os" - "testing" - fs "testing/fstest" - - "github.com/jackc/pgerrcode" - "github.com/jackc/pgx/v4" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/pkg/pgtest" -) - -//go:embed testdata/seed.sql -var testSeed string - -func TestPendingSeeds(t *testing.T) { - pending := []string{"testdata/seed.sql"} - - t.Run("finds new seeds", func(t *testing.T) { - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(SELECT_SEED_TABLE). - Reply("SELECT 0") - // Run test - seeds, err := GetPendingSeeds(context.Background(), pending, conn.MockClient(t), testMigrations) - // Check error - assert.NoError(t, err) - require.Len(t, seeds, 1) - assert.Equal(t, seeds[0].Path, pending[0]) - assert.Equal(t, seeds[0].Hash, "61868484fc0ddca2a2022217629a9fd9a4cf1ca479432046290797d6d40ffcc3") - assert.False(t, seeds[0].Dirty) - }) - - t.Run("finds dirty seeds", func(t *testing.T) { - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(SELECT_SEED_TABLE). - Reply("SELECT 1", SeedFile{Path: pending[0], Hash: "outdated"}) - // Run test - seeds, err := GetPendingSeeds(context.Background(), pending, conn.MockClient(t), testMigrations) - // Check error - assert.NoError(t, err) - require.Len(t, seeds, 1) - assert.Equal(t, seeds[0].Path, pending[0]) - assert.Equal(t, seeds[0].Hash, "61868484fc0ddca2a2022217629a9fd9a4cf1ca479432046290797d6d40ffcc3") - assert.True(t, seeds[0].Dirty) - }) - - t.Run("skips applied seed", func(t *testing.T) { - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(SELECT_SEED_TABLE). - Reply("SELECT 1", SeedFile{Path: pending[0], Hash: "61868484fc0ddca2a2022217629a9fd9a4cf1ca479432046290797d6d40ffcc3"}) - // Run test - seeds, err := GetPendingSeeds(context.Background(), pending, conn.MockClient(t), testMigrations) - // Check error - assert.NoError(t, err) - require.Empty(t, seeds) - }) - - t.Run("ignores missing seed table", func(t *testing.T) { - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(SELECT_SEED_TABLE). - ReplyError(pgerrcode.UndefinedTable, `relation "seed_files" does not exist`) - // Run test - _, err := GetPendingSeeds(context.Background(), pending, conn.MockClient(t), testMigrations) - // Check error - assert.NoError(t, err) - }) - - t.Run("ignores missing seed file", func(t *testing.T) { - // Setup in-memory fs - fsys := fs.MapFS{} - // Run test - _, err := GetPendingSeeds(context.Background(), pending, nil, fsys) - // Check error - assert.NoError(t, err) - }) -} - -func TestSeedData(t *testing.T) { - t.Run("seeds from file", func(t *testing.T) { - seed := SeedFile{ - Path: "testdata/seed.sql", - Hash: "61868484fc0ddca2a2022217629a9fd9a4cf1ca479432046290797d6d40ffcc3", - Dirty: true, - } - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - mockSeedHistory(conn). - Query(UPSERT_SEED_FILE, seed.Path, seed.Hash). - Reply("INSERT 0 1") - // Run test - err := SeedData(context.Background(), []SeedFile{seed}, conn.MockClient(t), testMigrations) - // Check error - assert.NoError(t, err) - }) - - t.Run("throws error on upsert failure", func(t *testing.T) { - seed := SeedFile{ - Path: "testdata/seed.sql", - Hash: "61868484fc0ddca2a2022217629a9fd9a4cf1ca479432046290797d6d40ffcc3", - } - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - mockSeedHistory(conn). - Query(testSeed+`;INSERT INTO supabase_migrations.seed_files(path, hash) VALUES( 'testdata/seed.sql' , '61868484fc0ddca2a2022217629a9fd9a4cf1ca479432046290797d6d40ffcc3' ) ON CONFLICT (path) DO UPDATE SET hash = EXCLUDED.hash`). - ReplyError(pgerrcode.NotNullViolation, `null value in column "age" of relation "employees"`) - // Run test - err := SeedData(context.Background(), []SeedFile{seed}, conn.MockClient(t, func(cc *pgx.ConnConfig) { - cc.PreferSimpleProtocol = true - }), testMigrations) - // Check error - assert.ErrorContains(t, err, `ERROR: null value in column "age" of relation "employees" (SQLSTATE 23502)`) - }) -} - -func mockSeedHistory(conn *pgtest.MockConn) *pgtest.MockConn { - conn.Query(SET_LOCK_TIMEOUT). - Query(CREATE_VERSION_SCHEMA). - Reply("CREATE SCHEMA"). - Query(CREATE_SEED_TABLE). - Reply("CREATE TABLE") - return conn -} - -//go:embed testdata/1_globals.sql -var testGlobals string - -func TestSeedGlobals(t *testing.T) { - pending := []string{"testdata/1_globals.sql"} - - t.Run("seeds from file", func(t *testing.T) { - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(testGlobals). - Reply("CREATE ROLE") - // Run test - err := SeedGlobals(context.Background(), pending, conn.MockClient(t), testMigrations) - // Check error - assert.NoError(t, err) - }) - - t.Run("throws error on missing file", func(t *testing.T) { - // Setup in-memory fs - fsys := fs.MapFS{} - // Run test - err := SeedGlobals(context.Background(), pending, nil, fsys) - // Check error - assert.ErrorIs(t, err, os.ErrNotExist) - }) - - t.Run("throws error on create failure", func(t *testing.T) { - // Setup mock postgres - conn := pgtest.NewConn() - defer conn.Close(t) - conn.Query(testGlobals). - ReplyError(pgerrcode.InvalidCatalogName, `database "postgres" does not exist`) - // Run test - err := SeedGlobals(context.Background(), pending, conn.MockClient(t), testMigrations) - // Check error - assert.ErrorContains(t, err, `ERROR: database "postgres" does not exist (SQLSTATE 3D000)`) - }) -} diff --git a/apps/cli-go/pkg/migration/testdata/0_schema.sql b/apps/cli-go/pkg/migration/testdata/0_schema.sql deleted file mode 100644 index ba3138abd8..0000000000 --- a/apps/cli-go/pkg/migration/testdata/0_schema.sql +++ /dev/null @@ -1 +0,0 @@ -create schema private \ No newline at end of file diff --git a/apps/cli-go/pkg/migration/testdata/1_globals.sql b/apps/cli-go/pkg/migration/testdata/1_globals.sql deleted file mode 100644 index 0e17ccd4d4..0000000000 --- a/apps/cli-go/pkg/migration/testdata/1_globals.sql +++ /dev/null @@ -1 +0,0 @@ -create role developer_role with login password 'developer' \ No newline at end of file diff --git a/apps/cli-go/pkg/migration/testdata/seed.sql b/apps/cli-go/pkg/migration/testdata/seed.sql deleted file mode 100644 index e48c726ce3..0000000000 --- a/apps/cli-go/pkg/migration/testdata/seed.sql +++ /dev/null @@ -1 +0,0 @@ -INSERT INTO employees(name) VALUES ('Alice') \ No newline at end of file diff --git a/apps/cli-go/pkg/parser/state.go b/apps/cli-go/pkg/parser/state.go deleted file mode 100644 index 0a4bce6662..0000000000 --- a/apps/cli-go/pkg/parser/state.go +++ /dev/null @@ -1,296 +0,0 @@ -package parser - -import ( - "bytes" - "strings" - "unicode" - "unicode/utf8" -) - -const ( - // Omit BEGIN to allow arbitrary whitespaces between BEGIN and ATOMIC keywords. - // This can fail if ATOMIC is used as column name because it is not a reserved - // keyword: https://www.postgresql.org/docs/current/sql-keywords-appendix.html - BEGIN_ATOMIC = "ATOMIC" - END_ATOMIC = "END" -) - -type State interface { - // Return nil to emit token - Next(r rune, data []byte) State -} - -// Initial state: ready to parse next token -type ReadyState struct{} - -func (s *ReadyState) Next(r rune, data []byte) State { - switch r { - case '$': - // A $ after an identifier rune continues the identifier (pending$$foo$), not a - // dollar quote. A digit counts too (1$$), unlike PostgreSQL; valid SQL never has that. - offset := len(data) - utf8.RuneLen(r) - if hasIdentifierRuneBefore(data, offset) { - return s - } - return &TagState{offset: offset} - case '\'': - fallthrough - case '"': - return &QuoteState{delimiter: r} - case '-': - return &CommentState{} - case '/': - return &BlockState{} - case '\\': - return &EscapeState{} - case ';': - // Emit token - return nil - case '(': - return &ParenState{prev: s} - case 'c': - fallthrough - case 'C': - if isBeginAtomic(data) { - return &AtomicState{prev: s, statementStart: len(data)} - } - } - return s -} - -func isBeginAtomic(data []byte) bool { - if !endsWithKeyword(data, BEGIN_ATOMIC) { - return false - } - prefix := bytes.TrimRight(data[:len(data)-len(BEGIN_ATOMIC)], sqlWhitespace) - return endsWithKeyword(prefix, "BEGIN") -} - -// PostgreSQL's scan.l treats every byte at or above 0x80 as an identifier/dollar-tag -// character (ident_cont/dolq_cont), whatever its Unicode category. -func isIdentifierRune(r rune) bool { - return r >= utf8.RuneSelf || unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '$' -} - -func hasIdentifierRuneBefore(data []byte, offset int) bool { - if offset <= 0 { - return false - } - r, _ := utf8.DecodeLastRune(data[:offset]) - return isIdentifierRune(r) -} - -func endsWithKeyword(data []byte, keyword string) bool { - offset := len(data) - len(keyword) - if offset < 0 || !strings.EqualFold(string(data[offset:]), keyword) { - return false - } - return !hasIdentifierRuneBefore(data, offset) -} - -const sqlWhitespace = " \t\n\r\f\v" - -func isSqlWhitespace(b byte) bool { - return bytes.IndexByte([]byte(sqlWhitespace), b) >= 0 -} - -func isCommentsAndWhitespace(text []byte) bool { - for i := 0; i < len(text); { - switch { - case isSqlWhitespace(text[i]): - i++ - case bytes.HasPrefix(text[i:], []byte("--")): - newline := bytes.IndexByte(text[i+2:], '\n') - if newline == -1 { - return true - } - i += 2 + newline + 1 - case bytes.HasPrefix(text[i:], []byte("/*")): - // Match BlockState's sliding-window scan so both agree on overlapping delimiters. - depth := 1 - i += 2 - for i < len(text) && depth > 0 { - switch { - case bytes.HasPrefix(text[i-1:], []byte("/*")): - depth++ - case bytes.HasPrefix(text[i-1:], []byte("*/")): - depth-- - } - i++ - } - if depth > 0 { - return true - } - default: - return false - } - } - return true -} - -// Opened a line comment -type CommentState struct{} - -func (s *CommentState) Next(r rune, data []byte) State { - if r == '-' { - // No characters are escaped in comments, which is the same as dollar - return &DollarState{delimiter: []byte{'\n'}} - } - // Break out of comment state - state := &ReadyState{} - return state.Next(r, data) -} - -// Opened a block comment -type BlockState struct { - depth int -} - -func (s *BlockState) Next(r rune, data []byte) State { - const open = "/*" - const close = "*/" - window := data[len(data)-2:] - if bytes.Equal(window, []byte(open)) { - s.depth += 1 - return s - } - if s.depth == 0 { - // Break out of block state - state := &ReadyState{} - return state.Next(r, data) - } - if bytes.Equal(window, []byte(close)) { - s.depth -= 1 - if s.depth == 0 { - return &ReadyState{} - } - } - return s -} - -// Opened a single quote ' or double quote " -type QuoteState struct { - delimiter rune - escape bool -} - -func (s *QuoteState) Next(r rune, data []byte) State { - if s.escape { - // Preserve escaped quote '' - if r == s.delimiter { - s.escape = false - return s - } - // Break out of quote state - state := &ReadyState{} - return state.Next(r, data) - } - if r == s.delimiter { - s.escape = true - } - return s -} - -// Opened a dollar quote, no characters are ever esacped. -type DollarState struct { - delimiter []byte -} - -func (s *DollarState) Next(r rune, data []byte) State { - window := data[len(data)-len(s.delimiter):] - if bytes.Equal(window, s.delimiter) { - // Break out of dollar state - return &ReadyState{} - } - return s -} - -// Opened a tag, ie. $tag$ -type TagState struct { - offset int -} - -func (s *TagState) Next(r rune, data []byte) State { - if r == '$' { - // Make a copy since the data slice may be overwritten - tag := data[s.offset:] - dollar := DollarState{ - delimiter: make([]byte, len(tag)), - } - copy(dollar.delimiter, tag) - return &dollar - } - // Valid tag: https://www.postgresql.org/docs/current/sql-syntax-lexical.html - if isIdentifierRune(r) { - return s - } - // Break out of tag state - state := &ReadyState{} - return state.Next(r, data) -} - -// Opened a \ escape -type EscapeState struct{} - -func (s *EscapeState) Next(r rune, data []byte) State { - return &ReadyState{} -} - -// Opened a parenthesis group -type ParenState struct { - prev State -} - -func (s *ParenState) Next(r rune, data []byte) State { - curr := s.prev.Next(r, data) - if curr == nil { - s.prev = &ReadyState{} - return s - } - s.prev = curr - if _, ok := s.prev.(*ReadyState); !ok { - return s - } - if r == ')' { - return &ReadyState{} - } - return s -} - -// Opened BEGIN ATOMIC function body -type AtomicState struct { - prev State - pendingEnd bool - statementStart int - statementHasContent bool -} - -func (s *AtomicState) Next(r rune, data []byte) State { - pendingEnd := s.pendingEnd - s.pendingEnd = false - if pendingEnd && !isIdentifierRune(r) { - return (&ReadyState{}).Next(r, data) - } - // An END inside a nested quote/comment doesn't count. - curr := s.prev.Next(r, data) - if curr == nil { - s.prev = &ReadyState{} - s.statementStart = len(data) - s.statementHasContent = false - return s - } - s.prev = curr - if _, ok := s.prev.(*ReadyState); !ok { - return s - } - // PostgreSQL requires each inner statement to end with ';', so the closing END is - // always the first token of a statement; a later END is expression text. - if !s.statementHasContent && endsWithKeyword(data, END_ATOMIC) { - if isCommentsAndWhitespace(data[s.statementStart : len(data)-len(END_ATOMIC)]) { - s.pendingEnd = true - } else { - s.statementHasContent = true - } - } - return s -} diff --git a/apps/cli-go/pkg/parser/state_test.go b/apps/cli-go/pkg/parser/state_test.go deleted file mode 100644 index 921a33b852..0000000000 --- a/apps/cli-go/pkg/parser/state_test.go +++ /dev/null @@ -1,293 +0,0 @@ -package parser - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func checkSplit(t *testing.T, sql []string) { - input := strings.Join(sql, "") - output, err := Split(strings.NewReader(input)) - require.NoError(t, err) - assert.ElementsMatch(t, sql, output) -} - -func TestLineComment(t *testing.T) { - t.Run("after separator", func(t *testing.T) { - sql := []string{"END;", "-- comment"} - checkSplit(t, sql) - }) - - t.Run("before separator", func(t *testing.T) { - sql := []string{"SELECT --; 1"} - checkSplit(t, sql) - }) - - t.Run("not started", func(t *testing.T) { - sql := []string{"- ;", "END"} - checkSplit(t, sql) - }) - - t.Run("between lines", func(t *testing.T) { - sql := []string{"-- /* \n;", " */ END"} - checkSplit(t, sql) - }) -} - -func TestBlockComment(t *testing.T) { - t.Run("contains separator", func(t *testing.T) { - sql := []string{"SELECT /* ; */ 1;"} - checkSplit(t, sql) - }) - - t.Run("nested block", func(t *testing.T) { - sql := []string{"SELECT /*; /*;*/ ;*/ 1"} - checkSplit(t, sql) - }) - - t.Run("not started", func(t *testing.T) { - sql := []string{"/ * ;", " */ END"} - checkSplit(t, sql) - }) -} - -func TestSeparator(t *testing.T) { - t.Run("no spaces", func(t *testing.T) { - sql := []string{";", "END;", ";"} - checkSplit(t, sql) - }) - - t.Run("between spaces", func(t *testing.T) { - sql := []string{"BEGIN ;", " END"} - checkSplit(t, sql) - }) - - t.Run("backslash escaped", func(t *testing.T) { - sql := []string{"\\;;", "\\;"} - checkSplit(t, sql) - }) -} - -func TestDollarQuote(t *testing.T) { - t.Run("named tag", func(t *testing.T) { - sql := []string{"$tag$ any ; string$tag$"} - checkSplit(t, sql) - }) - - t.Run("non-ASCII named tag", func(t *testing.T) { - for _, tag := range []string{"a²", "a😀", "á"} { - t.Run(tag, func(t *testing.T) { - checkSplit(t, []string{"$" + tag + "$ any ; END; string$" + tag + "$;", " SELECT 2;"}) - }) - } - }) - - t.Run("anonymous tag", func(t *testing.T) { - sql := []string{"$$\"Dane's horse\"$$"} - checkSplit(t, sql) - }) - - t.Run("not started", func(t *testing.T) { - sql := []string{"SELECT \"$\";", " $$"} - checkSplit(t, sql) - }) -} - -func TestSingleQuote(t *testing.T) { - t.Run("escapes separator", func(t *testing.T) { - sql := []string{"SELECT ';' 1"} - checkSplit(t, sql) - }) - - t.Run("preserves single quote", func(t *testing.T) { - sql := []string{"SELECT ';'';' 1"} - checkSplit(t, sql) - }) - - t.Run("literal backslash", func(t *testing.T) { - sql := []string{"SELECT '\\';", " 1'"} - checkSplit(t, sql) - }) -} - -func TestDoubleQuote(t *testing.T) { - t.Run("escapes separator", func(t *testing.T) { - sql := []string{`CREATE POLICY "cats;dogs" on cats_dogs;`, " END"} - checkSplit(t, sql) - }) - - t.Run("preserves single quote", func(t *testing.T) { - sql := []string{`CREATE POLICY "cat's and dog's" on cats_dogs;`, " END"} - checkSplit(t, sql) - }) - - t.Run("preserves double quote", func(t *testing.T) { - sql := []string{`CREATE POLICY "pet""name" on pets;`, " END"} - checkSplit(t, sql) - }) -} - -func TestParentheses(t *testing.T) { - t.Run("preserves parentheses", func(t *testing.T) { - sql := []string{ - `CREATE RULE notify_me AS ON UPDATE TO mytable DO (NOTIFY mytable; SELECT "1");`, - `SELECT 1`, - } - checkSplit(t, sql) - }) - - t.Run("ignores literals", func(t *testing.T) { - sql := []string{`CREATE RULE notify_me AS ON UPDATE TO mytable DO (-- ) -SELECT ')'; --- ) -) -`} - checkSplit(t, sql) - }) -} - -func TestBeginAtomic(t *testing.T) { - t.Run("inline body", func(t *testing.T) { - sql := []string{`CREATE FUNCTION add(a integer, b integer) RETURNS integer -LANGUAGE SQL -IMMUTABLE STRICT PARALLEL SAFE -RETURNS NULL ON NULL INPUT -BEGIN ATOMIC - SELECT 'add'; - SELECT a + b; -END;`} - checkSplit(t, sql) - }) - - t.Run("case insenstive", func(t *testing.T) { - sql := []string{"begin atomic; select 'end'; end"} - checkSplit(t, sql) - }) - - t.Run("ignores literals", func(t *testing.T) { - sql := []string{`CREATE FUNCTION test() BEGIN -ATOMIC-- END; --- END; -END -;`} - checkSplit(t, sql) - }) - - t.Run("ignores atomic in identifiers", func(t *testing.T) { - names := []string{ - "fn_atomic", - "atomic_fn", - "my_atomic_thing", - "xatomicx", - "fn_ATomiC", - } - for _, name := range names { - t.Run(name, func(t *testing.T) { - sql := []string{ - `CREATE OR REPLACE FUNCTION ` + name + `() -RETURNS void LANGUAGE plpgsql AS $$ -BEGIN - NULL; -END; -$$;`, - ` -SELECT 1;`, - } - checkSplit(t, sql) - }) - } - }) - - t.Run("ignores end inside identifiers", func(t *testing.T) { - for _, name := range []string{"pending", "pending_change", "append", "legend", "𐐀end", "😀end", "́end", "²end", "pending$$foo$"} { - t.Run(name, func(t *testing.T) { - body := `CREATE FUNCTION f() RETURNS int LANGUAGE sql BEGIN ATOMIC SELECT 1 AS ` + name + `; END;` - checkSplit(t, []string{body, ` SELECT 2;`}) - }) - } - }) - - t.Run("ignores identifiers starting with end", func(t *testing.T) { - for _, name := range []string{"endpoint", "end_date", "ended_at", "end𐐀", "end😀", "end́", "end²", "end$$foo$"} { - t.Run(name, func(t *testing.T) { - body := `CREATE FUNCTION f() RETURNS int LANGUAGE sql BEGIN ATOMIC SELECT ` + name + `; SELECT 1; END;` - checkSplit(t, []string{body, ` SELECT 2;`}) - }) - } - }) - - t.Run("ignores end inside inner statements", func(t *testing.T) { - for _, expr := range []string{ - "CASE WHEN true THEN 1 ELSE 0 END", - "case when true then 1 end", - "CASE WHEN true THEN 1 END AS ended", - "CASE WHEN CASE WHEN true THEN true END THEN 1 END", - "(CASE WHEN (true) THEN 1 END)", - "coalesce(CASE WHEN length('a') > 0 THEN 1 END, 0)", - "CASE(1)WHEN 1 THEN 1 END", - "1 AS case", - "1 case", - "1 AS end", - "1 end", - "'end'", - } { - t.Run(expr, func(t *testing.T) { - body := `CREATE FUNCTION f() RETURNS int LANGUAGE sql BEGIN ATOMIC SELECT ` + expr + `; SELECT 1; END;` - checkSplit(t, []string{body, ` SELECT 2;`}) - }) - } - }) - - t.Run("closes at end preceded only by comments", func(t *testing.T) { - for _, comment := range []string{"-- note END\n", "/* note; */ ", "\n/* a /* b; */ */ -- c\n"} { - t.Run(comment, func(t *testing.T) { - body := `CREATE FUNCTION f() RETURNS int LANGUAGE sql BEGIN ATOMIC SELECT 1; ` + comment + `END;` - checkSplit(t, []string{body, ` SELECT 2;`}) - }) - } - }) - - t.Run("ignores non-ASCII begin atomic lookalikes", func(t *testing.T) { - // atomıc (dotless ı) case-folds to ATOMIC but is a plain identifier in SQL. - checkSplit(t, []string{"BEGIN atomıc;", " SELECT 'end';", " SELECT 2;"}) - }) - - t.Run("closes atomic body at end right after a positional parameter", func(t *testing.T) { - checkSplit(t, []string{"CREATE FUNCTION f(int) RETURNS int LANGUAGE sql BEGIN ATOMIC SELECT $1;END;", " SELECT 2;"}) - }) - - t.Run("requires sql whitespace between begin and atomic", func(t *testing.T) { - for _, gap := range []string{"\u00A0", "\uFEFF", "\u0085"} { - t.Run(gap, func(t *testing.T) { - checkSplit(t, []string{"BEGIN " + gap + " ATOMIC;", " SELECT 1;", " end;", " SELECT 2;"}) - }) - } - }) - - t.Run("closes nested atomic body inside parentheses", func(t *testing.T) { - checkSplit(t, []string{"DO (BEGIN ATOMIC SELECT 1; END; );", " SELECT 2;"}) - }) - - t.Run("ignores end after overlapping block comment", func(t *testing.T) { - body := `CREATE FUNCTION f() RETURNS int LANGUAGE sql BEGIN ATOMIC SELECT 1; /* a /*/ b */ SELECT 2 END; SELECT 3; END;` - checkSplit(t, []string{body, ` SELECT 4;`}) - }) - - t.Run("does not treat schema-qualified atomic function names as begin atomic", func(t *testing.T) { - sql := []string{`CREATE OR REPLACE FUNCTION public.atomic_example() -RETURNS INTEGER -LANGUAGE plpgsql -AS $$ -BEGIN - RETURN 1; -END; -$$;`, - ` -GRANT EXECUTE ON FUNCTION public.atomic_example() TO authenticated;`, - } - checkSplit(t, sql) - }) -} diff --git a/apps/cli-go/pkg/parser/testdata/all.sql b/apps/cli-go/pkg/parser/testdata/all.sql deleted file mode 100644 index c63948869c..0000000000 --- a/apps/cli-go/pkg/parser/testdata/all.sql +++ /dev/null @@ -1,140 +0,0 @@ -CREATE SCHEMA "blocks"; -CREATE TABLE "blocks"."block" -( - uuid UUID PRIMARY KEY NOT NULL, - settings_type CHARACTER VARYING NOT NULL, - settings JSONB NOT NULL, - title CHARACTER VARYING, - view_type CHARACTER VARYING, - enabled BOOL DEFAULT TRUE NOT NULL, - visibility_settings JSONB -); - -CREATE UNIQUE INDEX "block_uuid_uindex" - ON "blocks"."block" (uuid); - -CREATE INDEX "enabled_index" - ON "blocks"."block" (enabled); - -CREATE TABLE "blocks"."placement" -( - location_name CHARACTER VARYING NOT NULL, - index INT NOT NULL, - block_uuid UUID NOT NULL, - scope_path CHARACTER VARYING NOT NULL, - inherited BOOL DEFAULT FALSE NOT NULL, - excluded_scopes CHARACTER VARYING [], - - -- NOTE: Because we UPDATE the primary (location_name, index) key on the placement-table, - -- the constraint behavior on the primary key is set to DEFERRABLE and INITIALLY IMMEDIATE. - -- - -- For more inforation, refer to this bug-report: - -- - -- https://www.postgresql.org/message-id/flat/20170322123053.1421.55154%40wrigleys.postgresql.org - - CONSTRAINT placement_location_name_index_pk PRIMARY KEY (location_name, index) DEFERRABLE INITIALLY IMMEDIATE, - CONSTRAINT placement_block_uuid_fk FOREIGN KEY (block_uuid) REFERENCES "blocks"."block" (uuid) ON DELETE CASCADE -); - -CREATE INDEX "placement_location_name_index_index" - ON "blocks".placement (location_name, index); - -CREATE INDEX "placement_scope_path_index" - ON "blocks".placement (scope_path); - -CREATE INDEX "placement_index_index" - ON "blocks".placement (index); - -CREATE TABLE "blocks"."location_hash" -( - location_name CHARACTER VARYING PRIMARY KEY NOT NULL, - hash CHARACTER VARYING NOT NULL -); - -CREATE TABLE "blocks"."child_blocks" -( - parent_uuid UUID NOT NULL, - column_index INT NOT NULL, - child_uuids UUID [] NOT NULL, - CONSTRAINT child_blocks_parent_uuid_index_pk PRIMARY KEY (parent_uuid, column_index), - CONSTRAINT child_blocks_block_uuid_fk FOREIGN KEY (parent_uuid) REFERENCES "blocks"."block" (uuid) ON DELETE CASCADE - - -- TODO add a trigger to sanitize child_uuids when a block record is deleted -); - -CREATE INDEX "child_blocks_parent_uuid_index" - ON "blocks"."child_blocks" (parent_uuid); - -CREATE TABLE "blocks"."user_bucket" -( - user_uuid UUID PRIMARY KEY NOT NULL, - block_uuids UUID [] NOT NULL -); - -CREATE UNIQUE INDEX "user_bucket_user_uuid_uindex" - ON "blocks"."user_bucket" (user_uuid); - -CREATE VIEW "blocks"."block_count" AS - SELECT - b."uuid" AS "block_uuid", - ( - (SELECT COUNT(*) - FROM "blocks"."placement" p - WHERE p."block_uuid" = b."uuid") - + - (SELECT COALESCE(SUM((SELECT COUNT(*) - FROM unnest(c."child_uuids") cb - WHERE cb = b."uuid")), 0) - FROM "blocks"."child_blocks" c) - ) AS "ref_count", - (SELECT COUNT(*) - FROM "blocks"."user_bucket" k - WHERE "uuid" = ANY (k."block_uuids")) AS "user_count" - FROM "blocks"."block" b; - -CREATE OR REPLACE FUNCTION "blocks".delete_orphaned_blocks() - RETURNS TRIGGER -AS $$ -BEGIN - LOOP - -- Note that RETURN QUERY does not return from the function - it works - -- more like the yield-statement in PHP, in that records from the - -- DELETE..RETURNING statement are returned, and execution then - -- resumes from the following statement. - - DELETE FROM "blocks"."block" b - WHERE b.uuid IN ( - SELECT c.block_uuid - FROM "blocks"."block_count" c - WHERE c.ref_count = 0 AND c.user_count = 0 - ); - - -- The FOUND flag is set TRUE/FALSE after executing a query - so we - -- EXIT from the LOOP block when the DELETE..RETURNING statement does - -- not delete and return any records. - - EXIT WHEN NOT FOUND; - END LOOP; - - RETURN NULL; -END; -$$ -LANGUAGE plpgsql; - -CREATE TRIGGER delete_orphans -AFTER UPDATE OR DELETE - ON "blocks"."user_bucket" -FOR EACH STATEMENT -EXECUTE PROCEDURE "blocks".delete_orphaned_blocks(); - -CREATE TRIGGER delete_orphans -AFTER UPDATE OR DELETE - ON "blocks"."placement" -FOR EACH STATEMENT -EXECUTE PROCEDURE "blocks".delete_orphaned_blocks(); - -CREATE TRIGGER delete_orphans -AFTER UPDATE OR DELETE - ON "blocks"."child_blocks" -FOR EACH STATEMENT -EXECUTE PROCEDURE "blocks".delete_orphaned_blocks(); diff --git a/apps/cli-go/pkg/parser/testdata/split_00.sql b/apps/cli-go/pkg/parser/testdata/split_00.sql deleted file mode 100644 index 78a47bc16e..0000000000 --- a/apps/cli-go/pkg/parser/testdata/split_00.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE SCHEMA "blocks"; \ No newline at end of file diff --git a/apps/cli-go/pkg/parser/testdata/split_01.sql b/apps/cli-go/pkg/parser/testdata/split_01.sql deleted file mode 100644 index a877e55f0d..0000000000 --- a/apps/cli-go/pkg/parser/testdata/split_01.sql +++ /dev/null @@ -1,11 +0,0 @@ - -CREATE TABLE "blocks"."block" -( - uuid UUID PRIMARY KEY NOT NULL, - settings_type CHARACTER VARYING NOT NULL, - settings JSONB NOT NULL, - title CHARACTER VARYING, - view_type CHARACTER VARYING, - enabled BOOL DEFAULT TRUE NOT NULL, - visibility_settings JSONB -); \ No newline at end of file diff --git a/apps/cli-go/pkg/parser/testdata/split_02.sql b/apps/cli-go/pkg/parser/testdata/split_02.sql deleted file mode 100644 index a4eecb94c7..0000000000 --- a/apps/cli-go/pkg/parser/testdata/split_02.sql +++ /dev/null @@ -1,4 +0,0 @@ - - -CREATE UNIQUE INDEX "block_uuid_uindex" - ON "blocks"."block" (uuid); \ No newline at end of file diff --git a/apps/cli-go/pkg/parser/testdata/split_03.sql b/apps/cli-go/pkg/parser/testdata/split_03.sql deleted file mode 100644 index 20eec9067c..0000000000 --- a/apps/cli-go/pkg/parser/testdata/split_03.sql +++ /dev/null @@ -1,4 +0,0 @@ - - -CREATE INDEX "enabled_index" - ON "blocks"."block" (enabled); \ No newline at end of file diff --git a/apps/cli-go/pkg/parser/testdata/split_04.sql b/apps/cli-go/pkg/parser/testdata/split_04.sql deleted file mode 100644 index 493f7aac38..0000000000 --- a/apps/cli-go/pkg/parser/testdata/split_04.sql +++ /dev/null @@ -1,21 +0,0 @@ - - -CREATE TABLE "blocks"."placement" -( - location_name CHARACTER VARYING NOT NULL, - index INT NOT NULL, - block_uuid UUID NOT NULL, - scope_path CHARACTER VARYING NOT NULL, - inherited BOOL DEFAULT FALSE NOT NULL, - excluded_scopes CHARACTER VARYING [], - - -- NOTE: Because we UPDATE the primary (location_name, index) key on the placement-table, - -- the constraint behavior on the primary key is set to DEFERRABLE and INITIALLY IMMEDIATE. - -- - -- For more inforation, refer to this bug-report: - -- - -- https://www.postgresql.org/message-id/flat/20170322123053.1421.55154%40wrigleys.postgresql.org - - CONSTRAINT placement_location_name_index_pk PRIMARY KEY (location_name, index) DEFERRABLE INITIALLY IMMEDIATE, - CONSTRAINT placement_block_uuid_fk FOREIGN KEY (block_uuid) REFERENCES "blocks"."block" (uuid) ON DELETE CASCADE -); \ No newline at end of file diff --git a/apps/cli-go/pkg/parser/testdata/split_05.sql b/apps/cli-go/pkg/parser/testdata/split_05.sql deleted file mode 100644 index 003b969530..0000000000 --- a/apps/cli-go/pkg/parser/testdata/split_05.sql +++ /dev/null @@ -1,4 +0,0 @@ - - -CREATE INDEX "placement_location_name_index_index" - ON "blocks".placement (location_name, index); \ No newline at end of file diff --git a/apps/cli-go/pkg/parser/testdata/split_06.sql b/apps/cli-go/pkg/parser/testdata/split_06.sql deleted file mode 100644 index c9b41508fb..0000000000 --- a/apps/cli-go/pkg/parser/testdata/split_06.sql +++ /dev/null @@ -1,4 +0,0 @@ - - -CREATE INDEX "placement_scope_path_index" - ON "blocks".placement (scope_path); \ No newline at end of file diff --git a/apps/cli-go/pkg/parser/testdata/split_07.sql b/apps/cli-go/pkg/parser/testdata/split_07.sql deleted file mode 100644 index 824464ab6d..0000000000 --- a/apps/cli-go/pkg/parser/testdata/split_07.sql +++ /dev/null @@ -1,4 +0,0 @@ - - -CREATE INDEX "placement_index_index" - ON "blocks".placement (index); \ No newline at end of file diff --git a/apps/cli-go/pkg/parser/testdata/split_08.sql b/apps/cli-go/pkg/parser/testdata/split_08.sql deleted file mode 100644 index db30a5dd52..0000000000 --- a/apps/cli-go/pkg/parser/testdata/split_08.sql +++ /dev/null @@ -1,7 +0,0 @@ - - -CREATE TABLE "blocks"."location_hash" -( - location_name CHARACTER VARYING PRIMARY KEY NOT NULL, - hash CHARACTER VARYING NOT NULL -); \ No newline at end of file diff --git a/apps/cli-go/pkg/parser/testdata/split_09.sql b/apps/cli-go/pkg/parser/testdata/split_09.sql deleted file mode 100644 index fad90ece22..0000000000 --- a/apps/cli-go/pkg/parser/testdata/split_09.sql +++ /dev/null @@ -1,12 +0,0 @@ - - -CREATE TABLE "blocks"."child_blocks" -( - parent_uuid UUID NOT NULL, - column_index INT NOT NULL, - child_uuids UUID [] NOT NULL, - CONSTRAINT child_blocks_parent_uuid_index_pk PRIMARY KEY (parent_uuid, column_index), - CONSTRAINT child_blocks_block_uuid_fk FOREIGN KEY (parent_uuid) REFERENCES "blocks"."block" (uuid) ON DELETE CASCADE - - -- TODO add a trigger to sanitize child_uuids when a block record is deleted -); \ No newline at end of file diff --git a/apps/cli-go/pkg/parser/testdata/split_10.sql b/apps/cli-go/pkg/parser/testdata/split_10.sql deleted file mode 100644 index 4abf69d4a0..0000000000 --- a/apps/cli-go/pkg/parser/testdata/split_10.sql +++ /dev/null @@ -1,4 +0,0 @@ - - -CREATE INDEX "child_blocks_parent_uuid_index" - ON "blocks"."child_blocks" (parent_uuid); \ No newline at end of file diff --git a/apps/cli-go/pkg/parser/testdata/split_11.sql b/apps/cli-go/pkg/parser/testdata/split_11.sql deleted file mode 100644 index 91fa4a3250..0000000000 --- a/apps/cli-go/pkg/parser/testdata/split_11.sql +++ /dev/null @@ -1,7 +0,0 @@ - - -CREATE TABLE "blocks"."user_bucket" -( - user_uuid UUID PRIMARY KEY NOT NULL, - block_uuids UUID [] NOT NULL -); \ No newline at end of file diff --git a/apps/cli-go/pkg/parser/testdata/split_12.sql b/apps/cli-go/pkg/parser/testdata/split_12.sql deleted file mode 100644 index 835d649b37..0000000000 --- a/apps/cli-go/pkg/parser/testdata/split_12.sql +++ /dev/null @@ -1,4 +0,0 @@ - - -CREATE UNIQUE INDEX "user_bucket_user_uuid_uindex" - ON "blocks"."user_bucket" (user_uuid); \ No newline at end of file diff --git a/apps/cli-go/pkg/parser/testdata/split_13.sql b/apps/cli-go/pkg/parser/testdata/split_13.sql deleted file mode 100644 index c39a217906..0000000000 --- a/apps/cli-go/pkg/parser/testdata/split_13.sql +++ /dev/null @@ -1,19 +0,0 @@ - - -CREATE VIEW "blocks"."block_count" AS - SELECT - b."uuid" AS "block_uuid", - ( - (SELECT COUNT(*) - FROM "blocks"."placement" p - WHERE p."block_uuid" = b."uuid") - + - (SELECT COALESCE(SUM((SELECT COUNT(*) - FROM unnest(c."child_uuids") cb - WHERE cb = b."uuid")), 0) - FROM "blocks"."child_blocks" c) - ) AS "ref_count", - (SELECT COUNT(*) - FROM "blocks"."user_bucket" k - WHERE "uuid" = ANY (k."block_uuids")) AS "user_count" - FROM "blocks"."block" b; \ No newline at end of file diff --git a/apps/cli-go/pkg/parser/testdata/split_14.sql b/apps/cli-go/pkg/parser/testdata/split_14.sql deleted file mode 100644 index 8e04845fba..0000000000 --- a/apps/cli-go/pkg/parser/testdata/split_14.sql +++ /dev/null @@ -1,30 +0,0 @@ - - -CREATE OR REPLACE FUNCTION "blocks".delete_orphaned_blocks() - RETURNS TRIGGER -AS $$ -BEGIN - LOOP - -- Note that RETURN QUERY does not return from the function - it works - -- more like the yield-statement in PHP, in that records from the - -- DELETE..RETURNING statement are returned, and execution then - -- resumes from the following statement. - - DELETE FROM "blocks"."block" b - WHERE b.uuid IN ( - SELECT c.block_uuid - FROM "blocks"."block_count" c - WHERE c.ref_count = 0 AND c.user_count = 0 - ); - - -- The FOUND flag is set TRUE/FALSE after executing a query - so we - -- EXIT from the LOOP block when the DELETE..RETURNING statement does - -- not delete and return any records. - - EXIT WHEN NOT FOUND; - END LOOP; - - RETURN NULL; -END; -$$ -LANGUAGE plpgsql; \ No newline at end of file diff --git a/apps/cli-go/pkg/parser/testdata/split_15.sql b/apps/cli-go/pkg/parser/testdata/split_15.sql deleted file mode 100644 index 58920aae41..0000000000 --- a/apps/cli-go/pkg/parser/testdata/split_15.sql +++ /dev/null @@ -1,7 +0,0 @@ - - -CREATE TRIGGER delete_orphans -AFTER UPDATE OR DELETE - ON "blocks"."user_bucket" -FOR EACH STATEMENT -EXECUTE PROCEDURE "blocks".delete_orphaned_blocks(); \ No newline at end of file diff --git a/apps/cli-go/pkg/parser/testdata/split_16.sql b/apps/cli-go/pkg/parser/testdata/split_16.sql deleted file mode 100644 index 10cdd16a18..0000000000 --- a/apps/cli-go/pkg/parser/testdata/split_16.sql +++ /dev/null @@ -1,7 +0,0 @@ - - -CREATE TRIGGER delete_orphans -AFTER UPDATE OR DELETE - ON "blocks"."placement" -FOR EACH STATEMENT -EXECUTE PROCEDURE "blocks".delete_orphaned_blocks(); \ No newline at end of file diff --git a/apps/cli-go/pkg/parser/testdata/split_17.sql b/apps/cli-go/pkg/parser/testdata/split_17.sql deleted file mode 100644 index a8c67d265d..0000000000 --- a/apps/cli-go/pkg/parser/testdata/split_17.sql +++ /dev/null @@ -1,7 +0,0 @@ - - -CREATE TRIGGER delete_orphans -AFTER UPDATE OR DELETE - ON "blocks"."child_blocks" -FOR EACH STATEMENT -EXECUTE PROCEDURE "blocks".delete_orphaned_blocks(); \ No newline at end of file diff --git a/apps/cli-go/pkg/parser/token.go b/apps/cli-go/pkg/parser/token.go deleted file mode 100644 index db00843424..0000000000 --- a/apps/cli-go/pkg/parser/token.go +++ /dev/null @@ -1,119 +0,0 @@ -package parser - -import ( - "bufio" - "io" - "strings" - "unicode/utf8" - - "github.com/go-errors/errors" - "github.com/spf13/viper" - "github.com/supabase/cli/pkg/cast" -) - -// Equal to `startBufSize` from `bufio/scan.go` -const startBufSize = 4096 - -// MaxScannerCapacity defaults to 64 * 1024 which is not enough for certain lines -// containing e.g. geographical data. 256K ought to be enough for anybody... -var MaxScannerCapacity = 256 * 1024 - -// State transition table for tokenizer: -// -// Ready -> Ready (default) -// Ready -> Error (on invalid syntax) -// Ready -> Done (on ;, emit token) -// Ready -> Done (on EOF, emit token) -// -// Ready -> Comment (on --) -// Comment -> Comment (default) -// Comment -> Ready (on \n) -// -// Ready -> Block (on /*) -// Block -> Block (on /*, +-depth) -// Block -> Ready (on */, depth 0) -// -// Ready -> Quote (on ') -// Quote -> Quote (on '', default) -// Quote -> Ready (on ') -// -// Ready -> Dollar (on $tag$) -// Dollar -> Dollar (default) -// Dollar -> Ready (on $tag$) -// -// Ready -> Escape (on \) -// Escape -> Ready (on next) -type tokenizer struct { - state State - last int -} - -func (t *tokenizer) ScanToken(data []byte, atEOF bool) (advance int, token []byte, err error) { - // If we requested more data, resume from last position. - for width := 1; t.last < len(data); t.last += width { - r, width := utf8.DecodeRune(data[t.last:]) - end := t.last + width - t.state = t.state.Next(r, data[:end]) - // Emit token - if t.state == nil { - t.last = 0 - t.state = &ReadyState{} - return end, data[:end], nil - } - } - if !atEOF || len(data) == 0 { - // Request more data or end the stream - return 0, nil, nil - } - // We're at EOF. If we have a final, non-terminated token, return it. - return len(data), data, nil -} - -// Use bufio.Scanner to split a PostgreSQL string into multiple statements. -// -// The core problem is to figure out whether the current ; separator is inside -// an escaped string literal. PostgreSQL has multiple ways of opening a string -// literal, $$, ', --, /*, etc. We use a FSM to guarantee these states are -// entered exclusively. If not in one of the above escape states, the next ; -// token can be parsed as statement separator. -// -// Each statement is split as it is, without removing comments or white spaces. -func Split(sql io.Reader, transform ...func(string) string) (stats []string, err error) { - t := tokenizer{state: &ReadyState{}} - scanner := bufio.NewScanner(sql) - - // Increase scanner capacity to support very long lines containing e.g. geodata - buf := make([]byte, startBufSize) - maxbuf := cast.UintToInt(viper.GetSizeInBytes("SCANNER_BUFFER_SIZE")) - if maxbuf == 0 { - maxbuf = MaxScannerCapacity - } - scanner.Buffer(buf, maxbuf) - scanner.Split(t.ScanToken) - - var token string - for scanner.Scan() { - token = scanner.Text() - trim := token - for _, apply := range transform { - trim = apply(trim) - } - if len(trim) > 0 { - stats = append(stats, trim) - } - } - err = scanner.Err() - if err != nil { - err = errors.Errorf("%w\nAfter statement %d: %s", err, len(stats), token) - } - if errors.Is(err, bufio.ErrTooLong) { - err = errors.Errorf("%w\nTry setting SUPABASE_SCANNER_BUFFER_SIZE=5MB (current size is %dKB)", err, maxbuf>>10) - } - return stats, err -} - -func SplitAndTrim(sql io.Reader) (stats []string, err error) { - return Split(sql, func(token string) string { - return strings.TrimRight(token, ";") - }, strings.TrimSpace) -} diff --git a/apps/cli-go/pkg/parser/token_test.go b/apps/cli-go/pkg/parser/token_test.go deleted file mode 100644 index 566b11867b..0000000000 --- a/apps/cli-go/pkg/parser/token_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package parser - -import ( - "bufio" - _ "embed" - "io/fs" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSplit(t *testing.T) { - const testdata = "testdata" - - var fixture []string - require.NoError(t, filepath.WalkDir(testdata, func(path string, f fs.DirEntry, err error) error { - if err != nil { - return err - } - if !strings.HasPrefix(f.Name(), "split_") { - return nil - } - contents, err := os.ReadFile(path) - if err != nil { - return err - } - fixture = append(fixture, string(contents)) - return nil - })) - require.Len(t, fixture, 18) - fixture = append(fixture, "\n") - - sql, err := os.Open(filepath.Join(testdata, "all.sql")) - require.NoError(t, err) - stats, err := Split(sql) - require.NoError(t, err) - - assert.ElementsMatch(t, fixture, stats) -} - -func TestSplitAndTrim(t *testing.T) { - sql := "\tBEGIN; " + strings.Repeat("a", MaxScannerCapacity) - stats, err := SplitAndTrim(strings.NewReader(sql)) - // Check error - assert.ErrorIs(t, err, bufio.ErrTooLong) - assert.ErrorContains(t, err, "After statement 1: \tBEGIN;") - assert.ElementsMatch(t, []string{"BEGIN"}, stats) -} diff --git a/apps/cli-go/pkg/pgtest/mock.go b/apps/cli-go/pkg/pgtest/mock.go deleted file mode 100644 index 999115b051..0000000000 --- a/apps/cli-go/pkg/pgtest/mock.go +++ /dev/null @@ -1,331 +0,0 @@ -package pgtest - -import ( - "context" - "fmt" - "net" - "reflect" - "testing" - "time" - - "github.com/jackc/pgmock" - "github.com/jackc/pgproto3/v2" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" - "github.com/supabase/cli/pkg/pgxv5" - "google.golang.org/grpc/test/bufconn" -) - -type MockConn struct { - client *pgx.Conn - - // Duplex server listener backed by in-memory buffer - server *bufconn.Listener - - // Mock server requests and responses - script pgmock.Script - - // Status parameters emitted by postgres on first connect - status map[string]string - - // Channel for reporting all server error - errChan chan error -} - -func (r *MockConn) getStartupMessage(config *pgx.ConnConfig) []pgmock.Step { - var steps []pgmock.Step - // Add auth message - params := map[string]string{"user": config.User} - if len(config.Database) > 0 { - params["database"] = config.Database - } - steps = append( - steps, - pgmock.ExpectMessage(&pgproto3.StartupMessage{ - ProtocolVersion: pgproto3.ProtocolVersionNumber, - Parameters: params, - }), - pgmock.SendMessage(&pgproto3.AuthenticationOk{}), - ) - // Add status message - r.status["session_authorization"] = config.User - for key, value := range r.status { - steps = append(steps, pgmock.SendMessage(&pgproto3.ParameterStatus{Name: key, Value: value})) - } - // Add ready message - steps = append( - steps, - pgmock.SendMessage(&pgproto3.BackendKeyData{ProcessID: 0, SecretKey: 0}), - pgmock.SendMessage(&pgproto3.ReadyForQuery{TxStatus: 'I'}), - ) - return steps -} - -// Configures pgx to use the mock dialer. -// -// The mock dialer provides a full duplex net.Conn backed by an in-memory buffer. -// It is implemented by grcp/test/bufconn package. -func (r *MockConn) Intercept(config *pgx.ConnConfig) { - // Override config for test - config.DialFunc = func(ctx context.Context, network, addr string) (net.Conn, error) { - return r.server.DialContext(ctx) - } - config.LookupFunc = func(ctx context.Context, host string) (addrs []string, err error) { - return []string{"127.0.0.1"}, nil - } - config.TLSConfig = nil - // Add startup message - r.script.Steps = append(r.getStartupMessage(config), r.script.Steps...) -} - -// Adds a simple query or prepared statement to the mock connection. -func (r *MockConn) Query(sql string, args ...any) *MockConn { - var oids []uint32 - var params [][]byte - for _, v := range args { - if value, oid := r.encodeValueArg(v); oid > 0 { - params = append(params, value) - oids = append(oids, oid) - } - } - r.script.Steps = append(r.script.Steps, ExpectQuery(sql, params, oids)) - return r -} - -// SimpleQuery adds a simple-protocol query to the mock connection. -func (r *MockConn) SimpleQuery(sql string) *MockConn { - r.script.Steps = append(r.script.Steps, ExpectSimpleQuery(sql)) - return r -} - -func (r *MockConn) encodeValueArg(v any) (value []byte, oid uint32) { - if v == nil { - return nil, pgtype.TextArrayOID - } - dt, ok := ci.DataTypeForValue(v) - if !ok { - r.errChan <- fmt.Errorf("no suitable type for arg: %v", v) - return nil, 0 - } - if err := dt.Value.Set(v); err != nil { - r.errChan <- fmt.Errorf("failed to set value: %w", err) - return nil, 0 - } - var err error - switch dt.OID { - case pgtype.TextOID: - value, err = (dt.Value).(pgtype.TextEncoder).EncodeText(ci, []byte{}) - default: - value, err = (dt.Value).(pgtype.BinaryEncoder).EncodeBinary(ci, []byte{}) - } - if err != nil { - r.errChan <- fmt.Errorf("failed to encode arg: %w", err) - return nil, 0 - } - return value, dt.OID -} - -func getDataTypeSize(v any) int16 { - t := reflect.TypeOf(v) - k := t.Kind() - if k < reflect.Int || k > reflect.Complex128 { - return -1 - } - return int16(t.Size()) -} - -func (r *MockConn) lastQuery() *extendedQueryStep { - return r.script.Steps[len(r.script.Steps)-1].(*extendedQueryStep) -} - -// Adds a server reply using binary or text protocol format. -// -// TODO: support prepared statements when using binary protocol -func (r *MockConn) Reply(tag string, rows ...any) *MockConn { - q := r.lastQuery() - // Add field description - if len(rows) > 0 { - var desc pgproto3.RowDescription - if arr, ok := rows[0].([]any); ok { - for i, v := range arr { - name := fmt.Sprintf("c_%02d", i) - if fd := toFieldDescription(v); fd != nil { - fd.Name = []byte(name) - desc.Fields = append(desc.Fields, *fd) - } else { - r.errChan <- fmt.Errorf("failed to describe field: %s", name) - } - } - } else if t := reflect.TypeOf(rows[0]); t.Kind() == reflect.Struct { - s := reflect.ValueOf(rows[0]) - for i := 0; i < s.NumField(); i++ { - name := pgxv5.GetColumnName(t.Field(i)) - if len(name) == 0 { - continue - } - v := s.Field(i).Interface() - if fd := toFieldDescription(v); fd != nil { - fd.Name = []byte(name) - desc.Fields = append(desc.Fields, *fd) - } else { - r.errChan <- fmt.Errorf("failed to describe field: %s", name) - } - } - } else { - r.errChan <- fmt.Errorf("reply type must be one of [array, struct], found: %s", t.Kind()) - } - q.reply.Steps = append(q.reply.Steps, pgmock.SendMessage(&desc)) - } else { - // No data is optional, but we add for completeness - q.reply.Steps = append(q.reply.Steps, pgmock.SendMessage(&pgproto3.NoData{})) - } - // Add row data - for _, data := range rows { - var dr pgproto3.DataRow - if arr, ok := data.([]any); ok { - for _, v := range arr { - if value, oid := r.encodeValueArg(v); oid > 0 { - dr.Values = append(dr.Values, value) - } - } - } else if t := reflect.TypeOf(data); t.Kind() == reflect.Struct { - s := reflect.ValueOf(rows[0]) - for i := 0; i < s.NumField(); i++ { - if name := pgxv5.GetColumnName(t.Field(i)); len(name) == 0 { - continue - } - v := s.Field(i).Interface() - if value, oid := r.encodeValueArg(v); oid > 0 { - dr.Values = append(dr.Values, value) - } - } - } else { - r.errChan <- fmt.Errorf("invalid reply value: %v", data) - } - q.reply.Steps = append(q.reply.Steps, pgmock.SendMessage(&dr)) - } - // Add completion message - var complete pgproto3.BackendMessage - if tag == "" { - complete = &pgproto3.EmptyQueryResponse{} - } else { - complete = &pgproto3.CommandComplete{CommandTag: []byte(tag)} - } - q.reply.Steps = append(q.reply.Steps, pgmock.SendMessage(complete)) - return r -} - -func toFieldDescription(v any) *pgproto3.FieldDescription { - if dt, ok := ci.DataTypeForValue(v); ok { - size := getDataTypeSize(v) - format := ci.ParamFormatCodeForOID(dt.OID) - return &pgproto3.FieldDescription{ - TableOID: 17131, - TableAttributeNumber: 1, - DataTypeOID: dt.OID, - DataTypeSize: size, - TypeModifier: -1, - Format: format, - } - } - return nil -} - -// Simulates an error reply from the server. -// -// TODO: simulate a notice reply -func (r *MockConn) ReplyError(code, message string) *MockConn { - q := r.lastQuery() - q.reply.Steps = append( - q.reply.Steps, - pgmock.SendMessage(&pgproto3.ErrorResponse{ - Severity: "ERROR", - SeverityUnlocalized: "ERROR", - Code: code, - Message: message, - }), - ) - return r -} - -func (r *MockConn) Close(t *testing.T) { - if r.client != nil { - if err := r.client.Close(context.Background()); err != nil { - t.Errorf("failed to close client: %v", err) - } - } - for err := range r.errChan { - t.Errorf("pgmock error:\n%v", err) - } - if err := r.server.Close(); err != nil { - t.Fatalf("failed to close server: %v", err) - } -} - -func (r *MockConn) MockClient(t *testing.T, opts ...func(*pgx.ConnConfig)) *pgx.Conn { - if r.client != nil { - return r.client - } - opts = append(opts, r.Intercept, func(cc *pgx.ConnConfig) { - cc.ConnectTimeout = time.Second * 2 - }) - var err error - if r.client, err = pgxv5.Connect(context.Background(), "", opts...); err != nil { - t.Errorf("failed to connect: %v", err) - } - return r.client -} - -func NewWithStatus(status map[string]string) *MockConn { - const bufSize = 1024 * 1024 - mock := MockConn{ - server: bufconn.Listen(bufSize), - status: status, - errChan: make(chan error, 10), - } - // Start server in background - const timeout = time.Millisecond * 450 - go func() { - defer close(mock.errChan) - // Block until we've opened a TCP connection - conn, err := mock.server.Accept() - if err != nil { - mock.errChan <- err - return - } - defer conn.Close() - // Prevent server from hanging the test - err = conn.SetDeadline(time.Now().Add(timeout)) - if err != nil { - mock.errChan <- err - return - } - // Always expect clients to terminate the request - mock.script.Steps = append(mock.script.Steps, ExpectTerminate()) - err = mock.script.Run(pgproto3.NewBackend(pgproto3.NewChunkReader(conn), conn)) - if err != nil { - mock.errChan <- err - return - } - }() - - return &mock -} - -func NewConn() *MockConn { - status := map[string]string{ - "application_name": "", - "client_encoding": "UTF8", - "DateStyle": "ISO, MDY", - "default_transaction_read_only": "off", - "in_hot_standby": "off", - "integer_datetimes": "on", - "IntervalStyle": "postgres", - "is_superuser": "on", - "server_encoding": "UTF8", - "server_version": "14.3 (Debian 14.3-1.pgdg110+1)", - "standard_conforming_strings": "on", - "TimeZone": "UTC", - } - return NewWithStatus(status) -} diff --git a/apps/cli-go/pkg/pgtest/step.go b/apps/cli-go/pkg/pgtest/step.go deleted file mode 100644 index a13d20d890..0000000000 --- a/apps/cli-go/pkg/pgtest/step.go +++ /dev/null @@ -1,141 +0,0 @@ -package pgtest - -import ( - "reflect" - - "github.com/go-errors/errors" - "github.com/jackc/pgmock" - "github.com/jackc/pgproto3/v2" - "github.com/jackc/pgtype" -) - -var ci = pgtype.NewConnInfo() - -type extendedQueryStep struct { - sql string - params [][]byte - oids []uint32 - simpleOnly bool - reply pgmock.Script -} - -func (e *extendedQueryStep) Step(backend *pgproto3.Backend) error { - msg, err := getFrontendMessage(backend) - if err != nil { - return err - } - - // Handle simple query - want := &pgproto3.Query{String: e.sql} - if m, ok := msg.(*pgproto3.Query); ok && reflect.DeepEqual(m, want) { - e.reply.Steps = append(e.reply.Steps, pgmock.SendMessage(&pgproto3.ReadyForQuery{TxStatus: 'I'})) - return e.reply.Run(backend) - } - if e.simpleOnly { - return errors.Errorf("expected => %#v\nactual => %#v", want, msg) - } - - // Handle prepared statements, name can be dynamic: lrupsc_5_0 - if m, ok := msg.(*pgproto3.Parse); ok { - want := &pgproto3.Parse{Name: m.Name, Query: e.sql, ParameterOIDs: m.ParameterOIDs} - if !reflect.DeepEqual(m, want) { - return errors.Errorf("expected => %#v\nactual => %#v", want, m) - } - // Anonymous ps falls through - if m.Name != "" { - script := pgmock.Script{Steps: []pgmock.Step{ - pgmock.ExpectMessage(&pgproto3.Describe{ObjectType: 'S', Name: m.Name}), - pgmock.ExpectMessage(&pgproto3.Sync{}), - pgmock.SendMessage(&pgproto3.ParseComplete{}), - pgmock.SendMessage(&pgproto3.ParameterDescription{ParameterOIDs: e.oids}), - // Postgres responds pgproto3.RowDescription but it's optional for pgx - pgmock.SendMessage(&pgproto3.ReadyForQuery{TxStatus: 'I'}), - }} - if err := script.Run(backend); err != nil { - return err - } - } - // Expect bind command next - msg, err = backend.Receive() - if err != nil { - return err - } - } - - if m, ok := msg.(*pgproto3.Bind); ok { - var codes []int16 - for _, oid := range e.oids { - codes = append(codes, ci.ParamFormatCodeForOID(oid)) - } - want := &pgproto3.Bind{ - ParameterFormatCodes: codes, - Parameters: e.params, - ResultFormatCodes: []int16{}, - DestinationPortal: m.DestinationPortal, - PreparedStatement: m.PreparedStatement, - } - if !reflect.DeepEqual(m, want) { - return errors.Errorf("expected => %#v\nactual => %#v", want, msg) - } - e.reply.Steps = append([]pgmock.Step{ - pgmock.ExpectMessage(&pgproto3.Describe{ObjectType: 'P'}), - pgmock.ExpectMessage(&pgproto3.Execute{}), - pgmock.SendMessage(&pgproto3.ParseComplete{}), - pgmock.SendMessage(&pgproto3.BindComplete{}), - }, e.reply.Steps...) - return e.reply.Run(backend) - } - - return errors.Errorf("expected => %#v\nactual => %#v", want, msg) -} - -// Expects a SQL query in any form: simple, prepared, or anonymous. -func ExpectQuery(sql string, params [][]byte, oids []uint32) pgmock.Step { - return &extendedQueryStep{sql: sql, params: params, oids: oids} -} - -// ExpectSimpleQuery expects SQL through the simple query protocol. -func ExpectSimpleQuery(sql string) pgmock.Step { - return &extendedQueryStep{sql: sql, simpleOnly: true} -} - -type terminateStep struct{} - -func (e *terminateStep) Step(backend *pgproto3.Backend) error { - msg, err := getFrontendMessage(backend) - if err != nil { - return err - } - - // Handle simple query - if _, ok := msg.(*pgproto3.Terminate); ok { - return nil - } - - return errors.Errorf("expected => %#v\nactual => %#v", &pgproto3.Terminate{}, msg) -} - -func ExpectTerminate() pgmock.Step { - return &terminateStep{} -} - -func getFrontendMessage(backend *pgproto3.Backend) (pgproto3.FrontendMessage, error) { - msg, err := backend.Receive() - if err != nil { - return nil, err - } - - // Sync signals end of batch statements - if _, ok := msg.(*pgproto3.Sync); ok { - reply := pgmock.SendMessage(&pgproto3.ReadyForQuery{TxStatus: 'I'}) - if err := reply.Step(backend); err != nil { - return nil, err - } - msg, err = backend.Receive() - if err != nil { - return nil, err - } - } - - return msg, nil -} diff --git a/apps/cli-go/pkg/pgxv5/connect.go b/apps/cli-go/pkg/pgxv5/connect.go deleted file mode 100644 index 4ae754f24e..0000000000 --- a/apps/cli-go/pkg/pgxv5/connect.go +++ /dev/null @@ -1,42 +0,0 @@ -package pgxv5 - -import ( - "context" - "fmt" - "os" - "strings" - - "github.com/go-errors/errors" - "github.com/jackc/pgconn" - "github.com/jackc/pgx/v4" -) - -// Extends pgx.Connect with support for programmatically overriding parsed config -func Connect(ctx context.Context, connString string, options ...func(*pgx.ConnConfig)) (*pgx.Conn, error) { - // Parse connection url - config, err := pgx.ParseConfig(connString) - if err != nil { - return nil, errors.Errorf("failed to parse connection string: %w", err) - } - config.OnNotice = func(pc *pgconn.PgConn, n *pgconn.Notice) { - if !shouldIgnore(n.Message) { - fmt.Fprintf(os.Stderr, "%s (%s): %s\n", n.Severity, n.Code, n.Message) - } - } - // Apply config overrides - for _, op := range options { - op(config) - } - // Connect to database - conn, err := pgx.ConnectConfig(ctx, config) - if err != nil { - return nil, errors.Errorf("failed to connect to postgres: %w", err) - } - return conn, nil -} - -func shouldIgnore(msg string) bool { - return strings.Contains(msg, `schema "supabase_migrations" already exists`) || - strings.Contains(msg, `relation "schema_migrations" already exists`) || - strings.Contains(msg, `relation "seed_files" already exists`) -} diff --git a/apps/cli-go/pkg/pgxv5/rows.go b/apps/cli-go/pkg/pgxv5/rows.go deleted file mode 100644 index 96aa16f5ac..0000000000 --- a/apps/cli-go/pkg/pgxv5/rows.go +++ /dev/null @@ -1,132 +0,0 @@ -// Backported from pgx/v5: https://github.com/jackc/pgx/blob/v5.3.1/rows.go#L408 -package pgxv5 - -import ( - "reflect" - "strings" - - "github.com/go-errors/errors" - "github.com/jackc/pgproto3/v2" - "github.com/jackc/pgx/v4" -) - -func CollectStrings(rows pgx.Rows) ([]string, error) { - defer rows.Close() - result := []string{} - for rows.Next() { - var version string - if err := rows.Scan(&version); err != nil { - return nil, errors.Errorf("failed to scan rows: %w", err) - } - result = append(result, version) - } - if err := rows.Err(); err != nil { - return nil, errors.Errorf("failed to parse rows: %w", err) - } - return result, nil -} - -// CollectRows iterates through rows, calling fn for each row, and collecting the results into a slice of T. -func CollectRows[T any](rows pgx.Rows) ([]T, error) { - defer rows.Close() - - slice := []T{} - - for rows.Next() { - var value T - if err := ScanRowToStruct(rows, &value); err != nil { - return nil, err - } - slice = append(slice, value) - } - - if err := rows.Err(); err != nil { - return nil, errors.Errorf("failed to collect rows: %w", err) - } - - return slice, nil -} - -func ScanRowToStruct(rows pgx.Rows, dst any) error { - dstValue := reflect.ValueOf(dst) - if dstValue.Kind() != reflect.Ptr { - return errors.Errorf("dst not a pointer") - } - - dstElemValue := dstValue.Elem() - scanTargets, err := appendScanTargets(dstElemValue, nil, rows.FieldDescriptions()) - if err != nil { - return err - } - - for i, t := range scanTargets { - if t == nil { - return errors.Errorf("struct doesn't have corresponding row field %s", rows.FieldDescriptions()[i].Name) - } - } - - if err := rows.Scan(scanTargets...); err != nil { - return errors.Errorf("failed to scan targets: %w", err) - } - return nil -} - -const structTagKey = "db" - -func fieldPosByName(fldDescs []pgproto3.FieldDescription, field string) (i int) { - i = -1 - for i, desc := range fldDescs { - if strings.EqualFold(string(desc.Name), field) { - return i - } - } - return -} - -func appendScanTargets(dstElemValue reflect.Value, scanTargets []any, fldDescs []pgproto3.FieldDescription) ([]any, error) { - var err error - dstElemType := dstElemValue.Type() - - if scanTargets == nil { - scanTargets = make([]any, len(fldDescs)) - } - - for i := 0; i < dstElemType.NumField(); i++ { - sf := dstElemType.Field(i) - if sf.PkgPath != "" && !sf.Anonymous { - // Field is unexported, skip it. - continue - } - // Handle anoymous struct embedding, but do not try to handle embedded pointers. - if sf.Anonymous && sf.Type.Kind() == reflect.Struct { - scanTargets, err = appendScanTargets(dstElemValue.Field(i), scanTargets, fldDescs) - if err != nil { - return nil, err - } - } else { - colName := GetColumnName(sf) - if len(colName) == 0 { - // Field is ignored, skip it. - continue - } - fpos := fieldPosByName(fldDescs, colName) - if fpos == -1 || fpos >= len(scanTargets) { - return nil, errors.Errorf("cannot find field %s in returned row", colName) - } - scanTargets[fpos] = dstElemValue.Field(i).Addr().Interface() - } - } - - return scanTargets, err -} - -func GetColumnName(sf reflect.StructField) string { - dbTag, dbTagPresent := sf.Tag.Lookup(structTagKey) - if !dbTagPresent { - return sf.Name - } - if dbTag = strings.Split(dbTag, ",")[0]; dbTag != "-" { - return dbTag - } - return "" -} diff --git a/apps/cli-go/pkg/queue/queue.go b/apps/cli-go/pkg/queue/queue.go deleted file mode 100644 index e427e01c53..0000000000 --- a/apps/cli-go/pkg/queue/queue.go +++ /dev/null @@ -1,57 +0,0 @@ -package queue - -import ( - "sync" - - "github.com/go-errors/errors" -) - -// JobQueue implements a background job processor using a single channel and wait group. -// -// The channel is initialised with a maximum number of concurrent workers. Adding a new job -// consumes a worker from the channel. After finishing a job, the worker is added back to -// the channel. When all workers are consumed, adding new job will block. -// -// Example usage: -// -// jq := NewJobQueue(5) -// err := jq.Put(func() error { -// return nil -// }) -// errors.Join(err, jq.Collect()) -type JobQueue struct { - wg sync.WaitGroup - errCh chan error -} - -func NewJobQueue(maxConcurrency uint) *JobQueue { - q := &JobQueue{ - errCh: make(chan error, maxConcurrency), - } - for i := 0; i < cap(q.errCh); i++ { - q.errCh <- nil - } - return q -} - -// Put runs a job in the background, returning any error from the previous job. -func (q *JobQueue) Put(job func() error) error { - err := <-q.errCh - q.wg.Add(1) - go func() { - defer q.wg.Done() - q.errCh <- job() - }() - return err -} - -// Collect waits for all jobs to finish, returning any errors. -func (q *JobQueue) Collect() error { - q.wg.Wait() - var err []error - for i := 0; i < cap(q.errCh); i++ { - err = append(err, <-q.errCh) - q.errCh <- nil - } - return errors.Join(err...) -} diff --git a/apps/cli-go/pkg/storage/analytics.go b/apps/cli-go/pkg/storage/analytics.go deleted file mode 100644 index 12f2abe013..0000000000 --- a/apps/cli-go/pkg/storage/analytics.go +++ /dev/null @@ -1,68 +0,0 @@ -package storage - -import ( - "context" - "fmt" - "net/http" - "os" - - "github.com/supabase/cli/pkg/fetcher" -) - -type AnalyticsBucketResponse struct { - Id string `json:"id"` // "test" - Name string `json:"name"` // "test" - CreatedAt string `json:"created_at"` // "2023-10-13T17:48:58.491Z" - UpdatedAt string `json:"updated_at"` // "2023-10-13T17:48:58.491Z" -} - -type CreateAnalyticsBucketRequest struct { - BucketName string `json:"bucketName"` -} - -func (s *StorageAPI) UpsertAnalyticsBuckets(ctx context.Context, bucketConfig map[string]struct{}, filter ...func(string) bool) error { - resp, err := s.Send(ctx, http.MethodGet, "/storage/v1/iceberg/bucket", nil) - if err != nil { - return err - } - buckets, err := fetcher.ParseJSON[[]AnalyticsBucketResponse](resp.Body) - if err != nil { - return err - } - var toDelete []string - exists := make(map[string]struct{}, len(buckets)) - for _, b := range buckets { - exists[b.Name] = struct{}{} - if _, ok := bucketConfig[b.Name]; !ok { - toDelete = append(toDelete, b.Name) - } - } - for name := range bucketConfig { - if _, ok := exists[name]; ok { - fmt.Fprintln(os.Stderr, "Bucket already exists:", name) - continue - } - fmt.Fprintln(os.Stderr, "Creating analytics bucket:", name) - body := CreateAnalyticsBucketRequest{BucketName: name} - if resp, err := s.Send(ctx, http.MethodPost, "/storage/v1/iceberg/bucket", body); err != nil { - return err - } else if err := resp.Body.Close(); err != nil { - fmt.Fprintln(os.Stderr, err) - } - } -OUTER: - for _, name := range toDelete { - for _, keep := range filter { - if !keep(name) { - continue OUTER - } - } - fmt.Fprintln(os.Stderr, "Pruning analytics bucket:", name) - if resp, err := s.Send(ctx, http.MethodDelete, "/storage/v1/iceberg/bucket/"+name, nil); err != nil { - return err - } else if err := resp.Body.Close(); err != nil { - fmt.Fprintln(os.Stderr, err) - } - } - return nil -} diff --git a/apps/cli-go/pkg/storage/api.go b/apps/cli-go/pkg/storage/api.go deleted file mode 100644 index a2af3588e3..0000000000 --- a/apps/cli-go/pkg/storage/api.go +++ /dev/null @@ -1,11 +0,0 @@ -package storage - -import "github.com/supabase/cli/pkg/fetcher" - -type StorageAPI struct { - *fetcher.Fetcher -} - -const PAGE_LIMIT = 100 - -const DELETE_OBJECTS_LIMIT = 1000 diff --git a/apps/cli-go/pkg/storage/batch.go b/apps/cli-go/pkg/storage/batch.go deleted file mode 100644 index bb6dad0b5d..0000000000 --- a/apps/cli-go/pkg/storage/batch.go +++ /dev/null @@ -1,131 +0,0 @@ -package storage - -import ( - "context" - "fmt" - "io/fs" - "os" - "path" - "path/filepath" - - "github.com/go-errors/errors" - "github.com/supabase/cli/pkg/config" - "github.com/supabase/cli/pkg/queue" -) - -func (s *StorageAPI) UpsertBuckets(ctx context.Context, bucketConfig config.BucketConfig, filter ...func(string) bool) error { - buckets, err := s.ListBuckets(ctx) - if err != nil { - return err - } - exists := make(map[string]string, len(buckets)) - for _, b := range buckets { - exists[b.Name] = b.Id - } -OUTER: - for name, bucket := range bucketConfig { - // Update bucket properties if already exists - if bucketId, ok := exists[name]; ok { - for _, keep := range filter { - if !keep(bucketId) { - continue OUTER - } - } - fmt.Fprintln(os.Stderr, "Updating Storage bucket:", bucketId) - body := UpdateBucketRequest{ - Id: bucketId, - Public: bucket.Public, - FileSizeLimit: int64(bucket.FileSizeLimit), - AllowedMimeTypes: bucket.AllowedMimeTypes, - } - if _, err := s.UpdateBucket(ctx, body); err != nil { - return err - } - } else { - fmt.Fprintln(os.Stderr, "Creating Storage bucket:", name) - body := CreateBucketRequest{ - Name: name, - Public: bucket.Public, - FileSizeLimit: int64(bucket.FileSizeLimit), - AllowedMimeTypes: bucket.AllowedMimeTypes, - } - if _, err := s.CreateBucket(ctx, body); err != nil { - return err - } - } - } - return nil -} - -type UploadOptions struct { - MaxConcurrency uint - KeyPrefix string -} - -func isUploadableEntry(fsys fs.FS, filePath string, info fs.DirEntry) bool { - if info.Type().IsRegular() { - return true - } - if info.Type().IsDir() { - return false - } - if info.Type()&fs.ModeSymlink != 0 { - f, err := fsys.Open(filePath) - if err != nil { - fmt.Fprintln(os.Stderr, "Skipping non-regular file:", filePath) - return false - } - defer f.Close() - stat, err := f.Stat() - if err != nil || !stat.Mode().IsRegular() { - fmt.Fprintln(os.Stderr, "Skipping non-regular file:", filePath) - return false - } - return true - } - fmt.Fprintln(os.Stderr, "Skipping non-regular file:", filePath) - return false -} - -func (s *StorageAPI) UpsertObjects(ctx context.Context, bucketConfig config.BucketConfig, fsys fs.FS, opts ...func(*UploadOptions)) error { - uo := UploadOptions{MaxConcurrency: 5} - for _, apply := range opts { - apply(&uo) - } - jq := queue.NewJobQueue(uo.MaxConcurrency) - for name, bucket := range bucketConfig { - localPath := bucket.ObjectsPath - if len(localPath) == 0 { - continue - } - upload := func(filePath string, info fs.DirEntry, err error) error { - if err != nil { - return errors.New(err) - } - if !isUploadableEntry(fsys, filePath, info) { - return nil - } - dstPath := uo.KeyPrefix - relPath, err := filepath.Rel(localPath, filePath) - if err != nil { - return errors.Errorf("failed to resolve relative path: %w", err) - } else if relPath == "." { - // Copying single file - dstPath = path.Join(name, info.Name()) - } else { - dstPath = path.Join(name, filepath.ToSlash(relPath)) - } - fmt.Fprintln(os.Stderr, "Uploading:", filePath, "=>", dstPath) - job := func() error { - return s.UploadObject(ctx, dstPath, filePath, fsys, func(fo *FileOptions) { - fo.Overwrite = true - }) - } - return jq.Put(job) - } - if err := fs.WalkDir(fsys, localPath, upload); err != nil { - return err - } - } - return jq.Collect() -} diff --git a/apps/cli-go/pkg/storage/buckets.go b/apps/cli-go/pkg/storage/buckets.go deleted file mode 100644 index 1f4bf9cc83..0000000000 --- a/apps/cli-go/pkg/storage/buckets.go +++ /dev/null @@ -1,78 +0,0 @@ -package storage - -import ( - "context" - "net/http" - - "github.com/supabase/cli/pkg/fetcher" -) - -type BucketResponse struct { - Id string `json:"id"` // "test" - Name string `json:"name"` // "test" - Owner string `json:"owner"` // "" - Public bool `json:"public"` // true - FileSizeLimit *int `json:"file_size_limit"` // null - AllowedMimeTypes []string `json:"allowed_mime_types"` // null - CreatedAt string `json:"created_at"` // "2023-10-13T17:48:58.491Z" - UpdatedAt string `json:"updated_at"` // "2023-10-13T17:48:58.491Z" -} - -func (s *StorageAPI) ListBuckets(ctx context.Context) ([]BucketResponse, error) { - resp, err := s.Send(ctx, http.MethodGet, "/storage/v1/bucket", nil) - if err != nil { - return nil, err - } - return fetcher.ParseJSON[[]BucketResponse](resp.Body) -} - -type CreateBucketRequest struct { - Name string `json:"name"` // "string", - Id string `json:"id,omitempty"` // "string", - Public *bool `json:"public,omitempty"` // false, - FileSizeLimit int64 `json:"file_size_limit,omitempty"` // 0, - AllowedMimeTypes []string `json:"allowed_mime_types,omitempty"` // ["string"] -} - -type CreateBucketResponse struct { - Name string `json:"name"` -} - -func (s *StorageAPI) CreateBucket(ctx context.Context, body CreateBucketRequest) (CreateBucketResponse, error) { - resp, err := s.Send(ctx, http.MethodPost, "/storage/v1/bucket", body) - if err != nil { - return CreateBucketResponse{}, err - } - return fetcher.ParseJSON[CreateBucketResponse](resp.Body) -} - -type UpdateBucketRequest struct { - Id string `json:"-"` - Public *bool `json:"public,omitempty"` // false, - FileSizeLimit int64 `json:"file_size_limit,omitempty"` // 0, - AllowedMimeTypes []string `json:"allowed_mime_types,omitempty"` // ["string"] -} - -type UpdateBucketResponse struct { - Message string `json:"message"` -} - -func (s *StorageAPI) UpdateBucket(ctx context.Context, body UpdateBucketRequest) (UpdateBucketResponse, error) { - resp, err := s.Send(ctx, http.MethodPut, "/storage/v1/bucket/"+body.Id, body) - if err != nil { - return UpdateBucketResponse{}, err - } - return fetcher.ParseJSON[UpdateBucketResponse](resp.Body) -} - -type DeleteBucketResponse struct { - Message string `json:"message"` -} - -func (s *StorageAPI) DeleteBucket(ctx context.Context, bucketId string) (DeleteBucketResponse, error) { - resp, err := s.Send(ctx, http.MethodDelete, "/storage/v1/bucket/"+bucketId, nil) - if err != nil { - return DeleteBucketResponse{}, err - } - return fetcher.ParseJSON[DeleteBucketResponse](resp.Body) -} diff --git a/apps/cli-go/pkg/storage/objects.go b/apps/cli-go/pkg/storage/objects.go deleted file mode 100644 index f338a63b2a..0000000000 --- a/apps/cli-go/pkg/storage/objects.go +++ /dev/null @@ -1,219 +0,0 @@ -package storage - -import ( - "context" - "io" - "io/fs" - "mime" - "net/http" - "os" - "path" - "path/filepath" - "strings" - - "github.com/go-errors/errors" - "github.com/spf13/afero" - "github.com/supabase/cli/pkg/fetcher" -) - -type ListObjectsQuery struct { - Prefix string `json:"prefix"` - Search string `json:"search,omitempty"` - Limit int `json:"limit,omitempty"` - Offset int `json:"offset,omitempty"` -} - -type ObjectResponse struct { - Name string `json:"name"` // "abstract.pdf" - Id *string `json:"id"` // "9b7f9f48-17a6-4ca8-b14a-39b0205a63e9" - UpdatedAt *string `json:"updated_at"` // "2023-10-13T18:08:22.068Z" - CreatedAt *string `json:"created_at"` // "2023-10-13T18:08:22.068Z" - LastAccessedAt *string `json:"last_accessed_at"` // "2023-10-13T18:08:22.068Z" - Metadata *ObjectMetadata `json:"metadata"` // null -} - -type ObjectMetadata struct { - ETag string `json:"eTag"` // "\"887ea9be3c68e6f2fca7fd2d7c77d8fe\"" - Size int `json:"size"` // 82702 - Mimetype string `json:"mimetype"` // "application/pdf" - CacheControl string `json:"cacheControl"` // "max-age=3600" - LastModified string `json:"lastModified"` // "2023-10-13T18:08:22.000Z" - ContentLength int `json:"contentLength"` // 82702 - HttpStatusCode int `json:"httpStatusCode"` // 200 -} - -func (s *StorageAPI) ListObjects(ctx context.Context, bucket, prefix string, page int) ([]ObjectResponse, error) { - dir, name := path.Split(prefix) - query := ListObjectsQuery{ - Prefix: dir, - Search: name, - Limit: PAGE_LIMIT, - Offset: PAGE_LIMIT * page, - } - resp, err := s.Send(ctx, http.MethodPost, "/storage/v1/object/list/"+bucket, query) - if err != nil { - return nil, err - } - return fetcher.ParseJSON[[]ObjectResponse](resp.Body) -} - -type FileOptions struct { - CacheControl string - ContentType string - Overwrite bool -} - -func ParseFileOptions(f fs.File, opts ...func(*FileOptions)) (*FileOptions, error) { - // Customise file options - fo := &FileOptions{} - for _, apply := range opts { - apply(fo) - } - // Use default value of storage-js: https://github.com/supabase/storage-js/blob/main/src/packages/StorageFileApi.ts#L22 - if len(fo.CacheControl) == 0 { - fo.CacheControl = "max-age=3600" - } - // Decode mimetype - if len(fo.ContentType) == 0 { - header := io.LimitReader(f, 512) - buf, err := io.ReadAll(header) - if err != nil { - return nil, errors.Errorf("failed to read file: %w", err) - } - fo.ContentType = http.DetectContentType(buf) - if s, ok := f.(io.Seeker); !ok { - return nil, errors.Errorf("file is not seekable") - } else if _, err = s.Seek(0, io.SeekStart); err != nil { - return nil, errors.Errorf("failed to seek file: %w", err) - } - } - return fo, nil -} - -func (s *StorageAPI) UploadObject(ctx context.Context, remotePath, localPath string, fsys fs.FS, opts ...func(*FileOptions)) error { - f, err := fsys.Open(localPath) - if err != nil { - return errors.Errorf("failed to open file: %w", err) - } - defer f.Close() - fo, err := ParseFileOptions(f, opts...) - if err != nil { - return err - } - // For text/plain content types, we try to determine a more specific type - // based on the file extension, as the initial detection might be too generic - if strings.Contains(fo.ContentType, "text/plain") { - if extensionType := mime.TypeByExtension(filepath.Ext(localPath)); extensionType != "" { - fo.ContentType = extensionType - } - } - return s.UploadObjectStream(ctx, remotePath, f, *fo) -} - -func (s *StorageAPI) UploadObjectStream(ctx context.Context, remotePath string, localFile io.Reader, fo FileOptions) error { - headers := func(req *http.Request) { - if len(fo.ContentType) > 0 { - req.Header.Add("Content-Type", fo.ContentType) - } - if len(fo.CacheControl) > 0 { - req.Header.Add("Cache-Control", fo.CacheControl) - } - if fo.Overwrite { - req.Header.Add("x-upsert", "true") - } - } - // Prepare request - remotePath = strings.TrimPrefix(remotePath, "/") - resp, err := s.Send(ctx, http.MethodPost, "/storage/v1/object/"+remotePath, io.NopCloser(localFile), headers) - if err != nil { - return err - } - defer resp.Body.Close() - return nil -} - -func (s *StorageAPI) DownloadObject(ctx context.Context, remotePath, localPath string, fsys afero.Fs) error { - f, err := fsys.OpenFile(localPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) - if err != nil { - return errors.Errorf("failed to create file: %w", err) - } - defer f.Close() - return s.DownloadObjectStream(ctx, remotePath, f) -} - -func (s *StorageAPI) DownloadObjectStream(ctx context.Context, remotePath string, localFile io.Writer) error { - remotePath = strings.TrimPrefix(remotePath, "/") - resp, err := s.Send(ctx, http.MethodGet, "/storage/v1/object/"+remotePath, nil) - if err != nil { - return err - } - defer resp.Body.Close() - _, err = io.Copy(localFile, resp.Body) - return err -} - -type MoveObjectRequest struct { - BucketId string `json:"bucketId"` - SourceKey string `json:"sourceKey"` - DestinationKey string `json:"destinationKey"` -} - -type MoveObjectResponse = DeleteBucketResponse - -func (s *StorageAPI) MoveObject(ctx context.Context, bucketId, srcPath, dstPath string) (MoveObjectResponse, error) { - body := MoveObjectRequest{ - BucketId: bucketId, - SourceKey: srcPath, - DestinationKey: dstPath, - } - resp, err := s.Send(ctx, http.MethodPost, "/storage/v1/object/move", body) - if err != nil { - return MoveObjectResponse{}, err - } - return fetcher.ParseJSON[MoveObjectResponse](resp.Body) -} - -type CopyObjectRequest = MoveObjectRequest - -type CopyObjectResponse struct { - Key string `json:"key"` -} - -func (s *StorageAPI) CopyObject(ctx context.Context, bucketId, srcPath, dstPath string) (CopyObjectResponse, error) { - body := CopyObjectRequest{ - BucketId: bucketId, - SourceKey: srcPath, - DestinationKey: dstPath, - } - resp, err := s.Send(ctx, http.MethodPost, "/storage/v1/object/copy", body) - if err != nil { - return CopyObjectResponse{}, err - } - return fetcher.ParseJSON[CopyObjectResponse](resp.Body) -} - -type DeleteObjectsRequest struct { - Prefixes []string `json:"prefixes"` -} - -type DeleteObjectsResponse struct { - BucketId string `json:"bucket_id"` // "private" - Owner string `json:"owner"` // "" - OwnerId string `json:"owner_id"` // "" - Version string `json:"version"` // "cf5c5c53-ee73-4806-84e3-7d92c954b436" - Name string `json:"name"` // "abstract.pdf" - Id string `json:"id"` // "9b7f9f48-17a6-4ca8-b14a-39b0205a63e9" - UpdatedAt string `json:"updated_at"` // "2023-10-13T18:08:22.068Z" - CreatedAt string `json:"created_at"` // "2023-10-13T18:08:22.068Z" - LastAccessedAt string `json:"last_accessed_at"` // "2023-10-13T18:08:22.068Z" - Metadata ObjectMetadata `json:"metadata"` // null -} - -func (s *StorageAPI) DeleteObjects(ctx context.Context, bucket string, prefixes []string) ([]DeleteObjectsResponse, error) { - body := DeleteObjectsRequest{Prefixes: prefixes} - resp, err := s.Send(ctx, http.MethodDelete, "/storage/v1/object/"+bucket, body) - if err != nil { - return nil, err - } - return fetcher.ParseJSON[[]DeleteObjectsResponse](resp.Body) -} diff --git a/apps/cli-go/pkg/storage/objects_test.go b/apps/cli-go/pkg/storage/objects_test.go deleted file mode 100644 index 8983495097..0000000000 --- a/apps/cli-go/pkg/storage/objects_test.go +++ /dev/null @@ -1,178 +0,0 @@ -package storage - -import ( - "bytes" - "context" - "io" - "mime" - "net/http" - "os" - "path/filepath" - "testing" - fs "testing/fstest" - - "github.com/h2non/gock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/pkg/config" - "github.com/supabase/cli/pkg/fetcher" -) - -var mockApi = StorageAPI{Fetcher: fetcher.NewFetcher( - "http://127.0.0.1", -)} - -func TestParseFileOptionsContentTypeDetection(t *testing.T) { - tests := []struct { - name string - content []byte - filename string - opts []func(*FileOptions) - wantMimeType string - wantCacheCtrl string - }{ - { - name: "detects PNG image", - content: []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}, // PNG header - filename: "test.image", - wantMimeType: "image/png", - wantCacheCtrl: "max-age=3600", - }, - { - name: "detects JavaScript file", - content: []byte("const hello = () => console.log('Hello, World!');"), - filename: "script.js", - wantMimeType: mime.TypeByExtension(".js"), - wantCacheCtrl: "max-age=3600", - }, - { - name: "detects CSS file", - content: []byte(".header { color: #333; font-size: 16px; }"), - filename: "styles.css", - wantMimeType: mime.TypeByExtension(".css"), - wantCacheCtrl: "max-age=3600", - }, - { - name: "detects SQL file", - content: []byte("SELECT * FROM users WHERE id = 1;"), - filename: "query.sql", - wantMimeType: mime.TypeByExtension(".sql"), - wantCacheCtrl: "max-age=3600", - }, - { - name: "use text/plain as fallback for unrecognized extensions", - content: []byte("const hello = () => console.log('Hello, World!');"), - filename: "main.nonexistent", - wantMimeType: "text/plain; charset=utf-8", - wantCacheCtrl: "max-age=3600", - }, - { - name: "respects custom content type", - content: []byte("const hello = () => console.log('Hello, World!');"), - filename: "custom.js", - wantMimeType: "application/custom", - wantCacheCtrl: "max-age=3600", - opts: []func(*FileOptions){func(fo *FileOptions) { fo.ContentType = "application/custom" }}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a temporary file with test content - fsys := fs.MapFS{tt.filename: &fs.MapFile{Data: tt.content}} - // Setup mock api - defer gock.OffAll() - gock.New("http://127.0.0.1"). - Post("/storage/v1/object/"+tt.filename). - MatchHeader("Content-Type", tt.wantMimeType). - MatchHeader("Cache-Control", tt.wantCacheCtrl). - Reply(http.StatusOK) - // Parse options - err := mockApi.UploadObject(context.Background(), tt.filename, tt.filename, fsys, tt.opts...) - // Assert results - assert.NoError(t, err) - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) - }) - } -} - -func TestUpsertObjects(t *testing.T) { - t.Run("uploads regular files", func(t *testing.T) { - defer gock.OffAll() - gock.New("http://127.0.0.1"). - Post("/storage/v1/object/uploads/pizza.jpg"). - Reply(http.StatusOK) - fsys := fs.MapFS{ - "seeds/pizza.jpg": &fs.MapFile{Data: []byte("image")}, - } - err := mockApi.UpsertObjects(context.Background(), config.BucketConfig{ - "uploads": {ObjectsPath: "seeds"}, - }, fsys) - assert.NoError(t, err) - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) - }) - - t.Run("uploads symlinked files", func(t *testing.T) { - tmpDir := t.TempDir() - fixturesDir := filepath.Join(tmpDir, "fixtures") - seedsDir := filepath.Join(tmpDir, "seeds") - require.NoError(t, os.MkdirAll(fixturesDir, 0o755)) - require.NoError(t, os.MkdirAll(seedsDir, 0o755)) - require.NoError(t, os.WriteFile(filepath.Join(fixturesDir, "pizza.jpg"), []byte("image"), 0o600)) - require.NoError(t, os.Symlink(filepath.Join(fixturesDir, "pizza.jpg"), filepath.Join(seedsDir, "pizza.jpg"))) - - defer gock.OffAll() - gock.New("http://127.0.0.1"). - Post("/storage/v1/object/uploads/pizza.jpg"). - Reply(http.StatusOK) - err := mockApi.UpsertObjects(context.Background(), config.BucketConfig{ - "uploads": {ObjectsPath: "seeds"}, - }, os.DirFS(tmpDir)) - assert.NoError(t, err) - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) - }) - - t.Run("skips broken symlinks with warning", func(t *testing.T) { - tmpDir := t.TempDir() - seedsDir := filepath.Join(tmpDir, "seeds") - require.NoError(t, os.MkdirAll(seedsDir, 0o755)) - require.NoError(t, os.Symlink(filepath.Join(tmpDir, "missing.jpg"), filepath.Join(seedsDir, "pizza.jpg"))) - - stderr := captureStderr(t, func() { - err := mockApi.UpsertObjects(context.Background(), config.BucketConfig{ - "uploads": {ObjectsPath: "seeds"}, - }, os.DirFS(tmpDir)) - assert.NoError(t, err) - }) - assert.Contains(t, stderr, "Skipping non-regular file:") - assert.Contains(t, stderr, "seeds/pizza.jpg") - }) -} - -func captureStderr(t *testing.T, fn func()) string { - t.Helper() - reader, writer, err := os.Pipe() - require.NoError(t, err) - original := os.Stderr - os.Stderr = writer - t.Cleanup(func() { - os.Stderr = original - _ = reader.Close() - _ = writer.Close() - }) - done := make(chan struct{}) - var buf bytes.Buffer - go func() { - _, _ = io.Copy(&buf, reader) - close(done) - }() - fn() - closeErr := writer.Close() - os.Stderr = original - <-done - require.NoError(t, closeErr) - return buf.String() -} diff --git a/apps/cli-go/pkg/storage/vector.go b/apps/cli-go/pkg/storage/vector.go deleted file mode 100644 index 7218f7b28b..0000000000 --- a/apps/cli-go/pkg/storage/vector.go +++ /dev/null @@ -1,81 +0,0 @@ -package storage - -import ( - "context" - "fmt" - "net/http" - "os" - - "github.com/supabase/cli/pkg/fetcher" -) - -type VectorBucket struct { - VectorBucketName string `json:"vectorBucketName"` - CreationTime uint64 `json:"creationTime"` -} - -type ListVectorBucketsResponse struct { - VectorBuckets []VectorBucket `json:"vectorBuckets"` -} - -type ListVectorBucketsRequest struct { - MaxResults uint64 `json:"maxResults,omitempty"` - NextToken string `json:"nextToken,omitempty"` - Prefix string `json:"prefix,omitempty"` -} - -type CreateVectorBucketRequest struct { - VectorBucketName string `json:"vectorBucketName"` -} - -type DeleteVectorBucketRequest struct { - VectorBucketName string `json:"vectorBucketName"` -} - -func (s *StorageAPI) UpsertVectorBuckets(ctx context.Context, bucketConfig map[string]struct{}, filter ...func(string) bool) error { - resp, err := s.Send(ctx, http.MethodPost, "/storage/v1/vector/ListVectorBuckets", ListVectorBucketsRequest{}) - if err != nil { - return err - } - result, err := fetcher.ParseJSON[ListVectorBucketsResponse](resp.Body) - if err != nil { - return err - } - var toDelete []string - exists := make(map[string]struct{}, len(result.VectorBuckets)) - for _, b := range result.VectorBuckets { - exists[b.VectorBucketName] = struct{}{} - if _, ok := bucketConfig[b.VectorBucketName]; !ok { - toDelete = append(toDelete, b.VectorBucketName) - } - } - for name := range bucketConfig { - if _, ok := exists[name]; ok { - fmt.Fprintln(os.Stderr, "Bucket already exists:", name) - continue - } - fmt.Fprintln(os.Stderr, "Creating vector bucket:", name) - body := CreateVectorBucketRequest{VectorBucketName: name} - if resp, err := s.Send(ctx, http.MethodPost, "/storage/v1/vector/CreateVectorBucket", body); err != nil { - return err - } else if err := resp.Body.Close(); err != nil { - fmt.Fprintln(os.Stderr, err) - } - } -OUTER: - for _, name := range toDelete { - for _, keep := range filter { - if !keep(name) { - continue OUTER - } - } - fmt.Fprintln(os.Stderr, "Pruning vector bucket:", name) - body := DeleteVectorBucketRequest{VectorBucketName: name} - if resp, err := s.Send(ctx, http.MethodPost, "/storage/v1/vector/DeleteVectorBucket", body); err != nil { - return err - } else if err := resp.Body.Close(); err != nil { - fmt.Fprintln(os.Stderr, err) - } - } - return nil -} diff --git a/apps/cli-go/pkg/vault/batch.go b/apps/cli-go/pkg/vault/batch.go deleted file mode 100644 index 8f44bb797e..0000000000 --- a/apps/cli-go/pkg/vault/batch.go +++ /dev/null @@ -1,60 +0,0 @@ -package vault - -import ( - "context" - "fmt" - "os" - - "github.com/go-errors/errors" - "github.com/jackc/pgx/v4" - "github.com/supabase/cli/pkg/config" - "github.com/supabase/cli/pkg/pgxv5" -) - -const ( - CREATE_VAULT_KV = "SELECT vault.create_secret($1, $2)" - READ_VAULT_KV = "SELECT id, name FROM vault.secrets WHERE name = ANY($1)" - UPDATE_VAULT_KV = "SELECT vault.update_secret($1, $2)" -) - -type VaultTable struct { - Id string - Name string -} - -func UpsertVaultSecrets(ctx context.Context, secrets map[string]config.Secret, conn *pgx.Conn) error { - var keys []string - toInsert := map[string]string{} - for k, v := range secrets { - if len(v.SHA256) > 0 { - keys = append(keys, k) - toInsert[k] = v.Value - } - } - if len(keys) == 0 { - return nil - } - fmt.Fprintln(os.Stderr, "Updating vault secrets...") - rows, err := conn.Query(ctx, READ_VAULT_KV, keys) - if err != nil { - return errors.Errorf("failed to read vault: %w", err) - } - toUpdate, err := pgxv5.CollectRows[VaultTable](rows) - if err != nil { - return err - } - batch := pgx.Batch{} - for _, r := range toUpdate { - secret := secrets[r.Name] - batch.Queue(UPDATE_VAULT_KV, r.Id, secret.Value) - delete(toInsert, r.Name) - } - // Remaining secrets should be created - for k, v := range toInsert { - batch.Queue(CREATE_VAULT_KV, v, k) - } - if err := conn.SendBatch(ctx, &batch).Close(); err != nil { - return errors.Errorf("failed to update vault: %w", err) - } - return nil -} diff --git a/apps/cli-go/tools/dump_initial_schema.sh b/apps/cli-go/tools/dump_initial_schema.sh deleted file mode 100755 index 1e07e1fcc2..0000000000 --- a/apps/cli-go/tools/dump_initial_schema.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# pg_dumpall -d "$DB_URL" -gf global.sql --no-role-passwords - -# Explanation of special flags: -# -# --column-inserts include migration history, but not pgsodium key -pg_dump \ - --column-inserts \ - --exclude-table-data 'pgsodium.key' \ - -d 'postgres://supabase_admin:postgres@127.0.0.1:54322/postgres' \ -| sed 's/^CREATE SCHEMA /CREATE SCHEMA IF NOT EXISTS /' \ -| sed 's/^CREATE TABLE /CREATE TABLE IF NOT EXISTS /' \ -| sed 's/^CREATE SEQUENCE /CREATE SEQUENCE IF NOT EXISTS /' \ -| sed 's/^CREATE VIEW /CREATE OR REPLACE VIEW /' \ -| sed 's/^CREATE FUNCTION /CREATE OR REPLACE FUNCTION /' \ -| sed 's/^GRANT ALL ON FUNCTION graphql_public./-- GRANT ALL ON FUNCTION graphql_public./' - -echo "DROP EXTENSION pg_graphql; CREATE EXTENSION pg_graphql WITH SCHEMA graphql;" - -# Reset session config generated by pg_dump -echo "RESET ALL;" diff --git a/apps/cli-go/tools/listdep/main.go b/apps/cli-go/tools/listdep/main.go deleted file mode 100644 index db72b7d429..0000000000 --- a/apps/cli-go/tools/listdep/main.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "encoding/json" - "log" - "os" - "strings" - - "github.com/supabase/cli/pkg/config" -) - -func main() { - external := make([]string, 0) - for _, img := range config.Images.Services() { - if !strings.HasPrefix(img, "supabase/") || - strings.HasPrefix(img, "supabase/logflare") { - external = append(external, img) - } - } - - enc := json.NewEncoder(os.Stdout) - if err := enc.Encode(external); err != nil { - log.Fatal(err) - } -} diff --git a/apps/cli/AGENTS.md b/apps/cli/AGENTS.md index 4322da3c07..d8d6ff4ab2 100644 --- a/apps/cli/AGENTS.md +++ b/apps/cli/AGENTS.md @@ -82,10 +82,83 @@ Every applicable command must preserve these invariants: `--output-format`, as established by `config diff` and `config pull`. 7. Preserve telemetry names, timing, identity, and payloads; see [Telemetry](#telemetry). +### Removing a command or flag + +Removal is breaking. First ship a stable deprecation warning on stderr that names the replacement +and the major that removes it, then remove on `next`. Replace a removed command with a tombstone +rather than deleting it; `db remote commit` is the reference: + +- Fail with `removedCommand(suggestion)` (or `removedFlag` for a flag) from + `command-internal/removed-command.ts`, wrapped in `withCommandTelemetry()` and + `withJsonErrorHandling`. Provide only `commandRuntimeLayer` and `telemetryStateLayer`. +- Keep the old flag and argument config so existing invocations reach the removal error, not a + parse error, and register the command with `Command.unlisted`. +- Delete the handler, its tests, and its docs pages, and drop it from `DOCS_EXCLUDED`. +- `SIDE_EFFECTS.md` documents exit 1, the removal message and suggestion, and the post-run + `telemetry.json` read and write under `SUPABASE_HOME`. +- Test every tombstone through the real parser with and without its old flags, asserting the + `RemovedSurfaceError` message and suggestion. + +## Config values + +Every config value resolves in one order for every command: flag > shell env > project `.env*` +(`supabase/` then the project root, by `SUPABASE_ENV`) > `config.toml` (a matched `[remotes.*]` +block over the base document) > default. Do not reorder it or special-case it per command. + +**Read a value** + +- Load once per command and pass the result down: `const configValues = yield* CliConfigValues`, + then `const resolvedConfig = yield* configValues.load({ workdir, projectRef })`. Do not reload a + resolved config a caller already holds. +- `resolvedConfig.get(CliConfigKeys.)` returns `{ value, origin }`; `origin.tier` names the + tier that won. +- For the whole document: `loaded` (what the project declares, every winner written in, no + defaults), `materialized.config` (`loaded` plus defaults and normalizers, what commands act on) + or `fileDeclared` (the config file alone, no flag or env overlay, for `config diff`/`pull`). + `envValues(names)` resolves `env(NAME)` references, shell before project `.env*`. +- Wrap writes that change config or `.temp` in `configValues.writeThrough(...)` so the memoised + resolved config is dropped. + +**Add a flag that sets a config value** + +1. List the name in `CLI_CONFIG_FLAGS` (`config/cli-config-key-annotations.ts`). +2. Declare it with `CliConfigKeys..flag({ name, description })` in the command config, never + a raw `Flag.*`. +3. Pipe the command config through `withCliConfigFlags` and provide `cliConfigValuesLayer` with + `Command.provide`. Read the value with `get`; never merge a flag into a value by hand. + +**Add a config key or env var** + +Add the field to `CliConfigSchema`; the registry key and its `SUPABASE_` env name +follow. Touch `config/cli-config-key-annotations.ts` only for a section gate, codec override, +secret or exclusion. Never read a `SUPABASE_*` name anywhere else. + +**Variables that are not config keys** + +Names outside the registry (libpq `PG*`, Docker, proxy, `SUPABASE_YES`) come from +`resolvedConfig.projectEnvValues` (project `.env*` only; a shell-set name is not in it) or +`ambientEnvironment()` (the live process env). Importing `ambientEnvironment` is limited by +`oxlint` to the audited files listed in `.oxlintrc.json`. Never read a registry name through +either, and never read `process.env`, `Bun.env` or `globalThis.process` directly. + +**What fails the build** + +`code-structure.unit.test.ts` (registry env names as literals, direct env reads, a raw `Flag.*` for +a registry flag, `loadCliConfig`, `resolveCliConfigSubtree` or `loadCliProjectEnvironment` outside +`config/cli-config-*.ts` and `shared/config/cli-config-*.ts`), `oxlint` (`process.env`, `Bun.env`, +`ambientEnvironment` imports), `cli-config-contract.unit.test.ts` and +`cli-config-flag-ownership.unit.test.ts` (every registry key and `CLI_CONFIG_FLAGS` flag against +the command tree), and `tsc` (a command that reads config without `withCliConfigFlags`). + +**Exceptions** + +They live in [ADR 0032](../../docs/adr/0032-config-value-precedence.md) and nowhere else. Do not add a +per-command exception without updating the ADR and the guard's exemption list together. + ## Experimental feature registration -Resolve opt-in booleans with `command-internal/experimental-feature.ts`: environment `1`/`0` -overrides the project setting, and an unset or empty value uses the config. Invalid environment +Resolve opt-in booleans with `shared/config/experimental-feature.ts`: a shell boolean +(`1`, `t`, `T`, `TRUE`, `true`, `True` / `0`, `f`, `F`, `FALSE`, `false`, `False`; other casings such as `tRue` fail) overrides the project setting, and an unset or empty value uses the config. Invalid environment values are typed failures on applicable command paths. Disabled families are absent from the command tree, help, and completion; enabled help is marked experimental and stays out of stable generated command documentation. Environment opt-ins do not write project configuration, except @@ -100,15 +173,6 @@ compatibility decision in the PR. For the Compute transition, no local compatibi migrations are required. Compute error tags intentionally start new fingerprints; server-owned contracts remain unchanged. Update tests, generated schemas, and side-effect documentation. -## Go delegation - -- TypeScript owns CLI behavior. Consult `apps/cli-go` only for existing delegated - operations listed in [the delegation document](docs/go-cli-porting-status.md); do not expand delegation. -- Native replacements must preserve the public command, flags, output, side effects, - and exit behavior. Update the delegation document when removing a Go dependency. -- Emit command telemetry exactly once: bare proxies rely on Go telemetry; - instrumented TypeScript handlers suppress child telemetry. - ## Telemetry > An error’s tag is the PostHog `error_fingerprint` identity (`tag:`); preserve it when @@ -122,10 +186,14 @@ property shape (`flags`, `is_agent`, and `env_signals`), use canonical names and [event catalog](src/shared/telemetry/event-catalog.ts), and redact sensitive values. `safeFlags` may include only values that carry no user data. Keep the established safe project, -project-id, org-id, and version cases. When `--project-ref` also accepts branch names, whitelist -only values matching `PROJECT_REF_PATTERN`; never log a user-created branch name. Global boolean -and choice flags are resolved by the wrapper from `globalFlagValues`; a command-local flag wins, -so a local path-valued `--output` remains redacted. Keep free-form user content redacted. +project-id, org-id, and version cases. For every command with a `hosted` or `selectable` target +policy, `withCommandTelemetry` reads the target flags from the invoked arguments, so they need no +entry in a command's `flags` or `safeFlags`: `--remote` (`true` when bare or linked, the project +ref when it is one, otherwise redacted), `--linked` and `--local` (their boolean), +`--project-ref`/`--project-id` (the value only when it matches `PROJECT_REF_PATTERN`, since it can +be a user-created branch name) and `--db-url` (always redacted). Global boolean and choice flags +are resolved by the wrapper from `globalFlagValues`; a command-local flag wins, so a local +path-valued `--output` remains redacted. Keep free-form user content redacted. Do not remove, rename, or reshape custom events. Preserve login aliasing after token persistence, link project and organization identity after the link write (including branch-link metadata), @@ -165,6 +233,31 @@ Plan-gated denials carry an optional `entitlement: { feature, upgrade_url }` on stream-json error object. `mapHttpError` attaches it from the Management API's `entitlement_required` body, so a handler that rebuilds a mapped status error must forward it. +## Targets and hosted consent + +Every leaf declares its target group and consent default with `withTargetPolicy`; the +classification test in `src/command-internal/target/target-policy.unit.test.ts` fails a leaf +without one. Declare the target flags only through `targetFlags` in +`src/command-internal/target/target-flags.ts`. + +- Database, storage, and seed commands call `resolveTarget`, then `resolveConnection`. Never pass a + default target: the policy owns the default, so a call site cannot fall back to local or linked. +- A command that binds `--password` passes `hasPasswordFlag: true` to `resolveConnection`. +- The linked `Target:` banner comes from `resolveTarget` and `resolveRef`; never build a linked + `ResolvedTarget` by hand. +- Hosted writes call `requireHostedMutationConsent` (or `requireTargetMutationConsent` when the + target may be local) after the plan and before the first persistent effect, and pass the + returned `HostedMutationConsent` to anything that needs proof of it. +- Never read stdin for consent. JSON, piped, and agent runs need `--yes` or a shell + `SUPABASE_YES`; `supabase/.env` and a cwd `.env*` file do not count. Write the action as an + imperative sentence-case phrase with no trailing period (`Delete project `). +- A `selectable` leaf with `consent: "none"` declares a `consentExemptReason`. +- Every leaf with consent has a `describeHostedConsent("", …)` test from + `tests/helpers/hosted-consent.ts`. + +See [ADR 0033](../../docs/adr/0033-target-selection-and-hosted-mutation-consent.md) for the +target-default and consent rules. + ## Tests Follow the root testing philosophy and use the package scripts in `package.json`. Cover meaningful diff --git a/apps/cli/README.md b/apps/cli/README.md index e379c971f5..5ad74ae94b 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -14,18 +14,12 @@ This workspace contains: This workspace contains the stable, shipped `supabase` CLI. Earlier revisions carried two shells (a `legacy/` tree and an experimental `next/` tree); both are gone and `src/` is the single CLI tree. -For current migration/parity status, see: - -- [`docs/go-cli-porting-status.md`](./docs/go-cli-porting-status.md) — the residual Go delegation surface -- [`docs/go-cli-divergences.md`](./docs/go-cli-divergences.md) — TS-only flags and behavioral divergences from the old Go CLI - For the generated command/reference docs, see: -- [`docs/go-cli-reference.md`](./docs/go-cli-reference.md) - [`docs/supabase-home.md`](./docs/supabase-home.md) - [`../../packages/stack/docs/service-versioning.md`](../../packages/stack/docs/service-versioning.md) -The README is intentionally brief. Command details should live in the generated docs and the parity tracker above. +The README is intentionally brief. Command details should live in the generated docs above. ## Run From Source @@ -42,9 +36,7 @@ Examples: pnpm dev -- hello ``` -### CLI and the Go binary - -Phase 0 commands in the CLI proxy to the Go CLI binary. To run these commands from source you need `supabase` (the Go CLI) available on your PATH. +### Running from source For convenience, create a shell alias instead of using `pnpm dev` directly. For example in `.zshrc`: @@ -52,19 +44,6 @@ For convenience, create a shell alias instead of using `pnpm dev` directly. For alias supabase-dev="bun /absolute/path/to/dx-lab/apps/cli/src/main.ts" ``` -Then Phase 0 commands resolve the Go binary via PATH automatically: - -```sh -supabase-dev orgs list # proxied to supabase on PATH -supabase-dev login # native TypeScript -``` - -You can also point `SUPABASE_GO_BINARY` at a specific binary to skip the PATH lookup: - -```sh -export SUPABASE_GO_BINARY=/path/to/supabase -``` - ## Build There are two separate build paths depending on what you need. @@ -91,11 +70,10 @@ The shim resolves `SUPABASE_CLI_BINARY_OVERRIDE` (an absolute binary path) befor Used at release time to produce the compiled binaries that go into the platform-specific npm packages: ```sh -# CLI (TS SFE + Go binary for each platform) bun scripts/build.ts --version X.Y.Z ``` -For the CLI, this also cross-compiles the Go CLI binary from `apps/cli-go/` and places both binaries in `packages/cli-{platform}/bin/`. +This cross-compiles the CLI binary for each platform and places it in `packages/cli-{platform}/bin/`. See [`docs/binary-distribution.md`](./docs/binary-distribution.md) for a full explanation of the packaging model. @@ -165,11 +143,8 @@ Platform-specific packages live under: - `packages/cli-linux-*` - `packages/cli-windows-*` -Each platform package ships two binaries for the stable channel: +Each platform package ships a single compiled binary for the stable channel: - `bin/supabase` — the compiled TypeScript SFE (Bun single-file executable) -- `bin/supabase-go` — the compiled Go CLI binary, used by Phase 0 proxy commands - -The Go binary is compiled from `apps/cli-go/` at release time. Run `pnpm repos:install` after a fresh clone to make that source available. See [`docs/binary-distribution.md`](./docs/binary-distribution.md) for the full packaging model. diff --git a/apps/cli/docs/analytics.md b/apps/cli/docs/analytics.md index 225713f0fc..3d8bbdd5fb 100644 --- a/apps/cli/docs/analytics.md +++ b/apps/cli/docs/analytics.md @@ -49,6 +49,12 @@ installs the command span plus the per-invocation analytics context: That same context is then inherited by milestone events captured inside the command handler, which lets one CLI invocation share a single `command_run_id`. +The wrapper also rejects the removed global `--create-ticket` flag (with any value) before the +handler runs. The rejection happens inside the instrumented region, so it still emits +`cli_command_executed` with an `error_fingerprint` ending in `:removed_flag`. A command that fails +earlier, such as on a missing credential in its runtime layer, or that shows help, reports that +instead. + ## Event Model The primary analytics event is: @@ -79,9 +85,8 @@ and the KPI query semantics (strict recovery, repeat errors, internal/unknown bug rate) are documented there in `CliErrorActionabilityMetricDefinitions`. A `workflow` property is reserved in the catalog but not emitted yet. -Not every failure is classified: pure Go-proxy commands report through the Go -binary, which does not emit these fields, and events from CLI versions before -they existed never carry them. KPI queries therefore scope to +Not every failure is classified: events from CLI versions before these fields +existed never carry them. KPI queries therefore scope to `error_kind IS NOT NULL`, and the `classificationCoverage` metric definition reports the classified share of failures so the covered fraction is explicit rather than assumed. @@ -91,11 +96,11 @@ Local-runtime commands also carry properties that split error rates by runtime: - `stack_backend` (`legacy` or `stack`) on commands routed by `experimental.stack`; see [backend selection](stack-commands.md) - `stack_runtime` (`docker`, `podman`, or `native`): the runtime of the stack a stack-backend - command looked up, which for `--db-url` may be the project stack even when the URL targets + command looked up, which for `--remote=` may be the project stack even when the URL targets another database - `orioledb` (boolean) once the command resolves local database config, `true` when the project selects an OrioleDB image (`db.orioledb_version` on a 15/17 project). It describes local config - only; for linked or `--db-url` targets it says nothing about the remote database + only; for `--remote` targets it says nothing about the remote database Flag capture is intentionally conservative: `flags` records only flags changed on the command line, and redacts each value unless it is boolean, a choice flag, or listed in the command's diff --git a/apps/cli/docs/binary-distribution.md b/apps/cli/docs/binary-distribution.md index dce49f57a9..1c9705a51d 100644 --- a/apps/cli/docs/binary-distribution.md +++ b/apps/cli/docs/binary-distribution.md @@ -1,87 +1,52 @@ # Binary Distribution -This document explains how the Supabase CLI is packaged and distributed, covering the two-binary model used by the CLI. +This document explains how the Supabase CLI is packaged and distributed as a compiled binary per platform. ## Overview -The CLI is distributed as a set of platform-specific npm packages. Each platform package contains two binaries: +The CLI is distributed as a set of platform-specific npm packages. Each platform package contains one binary: ``` @supabase/cli-darwin-arm64/ └── bin/ - ├── supabase ← TypeScript CLI (Bun single-file executable) - └── supabase-go ← Go CLI binary (residual proxy commands only) + └── supabase ← TypeScript CLI (Bun single-file executable) ``` The base `supabase` package routes to the correct platform package via `src/shared/cli/bin.ts`, which resolves and `execFileSync`s the platform-specific `bin/supabase` binary. -## Why Two Binaries - -The CLI was built as a gradual TypeScript port of the Go CLI, moving each command through two phases: - -- **Phase 0** — The command is defined in the TS CLI tree but proxied to the Go binary at runtime via `GoProxy`. -- **Phase 1+** — The command is implemented natively in TypeScript. - -That port is complete (CLI-1970). `supabase-go` is the residual proxy target for a fixed, small command surface: `db diff` (for `--use-pg-schema`), the Go-deprecated `db branch`/`db remote changes` command families, `gen keys`, and `functions download` (for the hidden `--legacy-bundle` path). See "Go binary command surface" under Release Workflow below for the full list and why each command stays. Two lifecycles apply here and must not be conflated: the **public commands** in that surface are retained (dropping them was ruled a breaking change — CLI-1964), while their **Go implementations** are slated for eventual native TS replacement, after which `supabase-go` stops shipping. Until the proxied surface is empty, the TS binary (`supabase`) still needs `supabase-go` available on the same system for those invocations. Every other Go command from the original CLI has been deleted outright from `apps/cli-go/`, not merely excluded from the build — see the same section for how. - ## Package Layout ``` packages/ - cli-darwin-arm64/bin/ supabase + supabase-go - cli-darwin-x64/bin/ supabase + supabase-go - cli-linux-arm64/bin/ supabase + supabase-go - cli-linux-x64/bin/ supabase + supabase-go - cli-linux-arm64-musl/bin/ supabase (musl TS binary only) - cli-linux-x64-musl/bin/ supabase (musl TS binary only) - cli-windows-arm64/bin/ supabase.exe + supabase-go.exe - cli-windows-x64/bin/ supabase.exe + supabase-go.exe + cli-darwin-arm64/bin/ supabase + cli-darwin-x64/bin/ supabase + cli-linux-arm64/bin/ supabase + cli-linux-x64/bin/ supabase + cli-linux-arm64-musl/bin/ supabase + cli-linux-x64-musl/bin/ supabase + cli-windows-arm64/bin/ supabase.exe + cli-windows-x64/bin/ supabase.exe ``` -The musl packages only carry the Bun TS binary (compiled for musl). The Go binary is statically linked (`CGO_ENABLED=0`), so the glibc Linux binary works on musl as well — it is installed alongside the musl TS binary by the Linux package managers (deb/rpm/apk) from the glibc build. - -## Runtime Resolution - -When a Phase 0 command runs, `go-proxy.layer.ts` resolves the Go binary in this order: - -1. **`SUPABASE_GO_BINARY` env var** — explicit override, takes priority. -2. **Co-located `supabase-go`** — looks next to `process.execPath`. Works in compiled SFE mode because the base shim uses `execFileSync`, making the TS SFE the main process with `process.execPath` pointing to itself. This is also how PATH-installed setups resolve: `supabase-go` ships next to the `supabase` shim. -3. **npm package resolution** — resolves `@supabase/cli-{platform}/bin/supabase-go`. Works when running from source with the platform packages installed. - -There is deliberately **no** `PATH` fallback (CLI-1488): the `supabase` shim itself is what's on `PATH`, so falling back to it would re-invoke the shim and fork-bomb. Resolution failure is a hard error with install guidance. - -## Source of the Go Binary - -The Go CLI source lives in `apps/cli-go/` and is managed via: - -```sh -pnpm repos:install -``` - -This must be run after a fresh clone before building a release. - ## Development Workflow -No build step is required to run the CLI from source, but the Go binary must be resolvable — easiest via `SUPABASE_GO_BINARY` (below) or an installed `@supabase/cli-` package. +No build step is required to run the CLI from source. -1. Build the Go binary once: `cd apps/cli-go && go build -o supabase-go .` -2. Create a shell alias to run the CLI from source. For example in `.zshrc`: +1. Create a shell alias to run the CLI from source. For example in `.zshrc`: ```sh alias supabase-dev="bun /path/to/dx-lab/apps/cli/src/main.ts" ``` -3. Point `SUPABASE_GO_BINARY` at the built binary and run commands: +2. Run commands directly: ```sh - export SUPABASE_GO_BINARY=/path/to/apps/cli-go/supabase-go - supabase-dev db branch list # proxied to the Go binary - supabase-dev login # native TypeScript implementation + supabase-dev login ``` ## Release Workflow -The `scripts/build.ts` script compiles both binaries for all target platforms: +The `scripts/build.ts` script compiles the binary for all target platforms: ```sh bun scripts/build.ts --version X.Y.Z @@ -90,51 +55,9 @@ bun scripts/build.ts --version X.Y.Z This: 1. Compiles the TS CLI to a Bun SFE for each platform → `packages/cli-{platform}/bin/supabase` -2. Cross-compiles the Go CLI (`CGO_ENABLED=0`) for each platform → `packages/cli-{platform}/bin/supabase-go` -3. Signs the macOS binaries (both `supabase` and `supabase-go`) before archiving, so every channel ships the signed bytes — see [release-process.md § Code signing (macOS)](./release-process.md#code-signing-macos) and [ADR 0014](../../../docs/adr/0014-macos-code-signing-and-notarization.md) -4. Bundles both binaries into the platform archives (`.tar.gz` / `.zip`) -5. Includes both binaries in the Linux package manager packages (deb/rpm/apk) - -### Go binary command surface - -`supabase-go` does not ship the full old Go CLI — only the fixed subset the TypeScript CLI still proxies to via `GoProxy`: - -- `db diff` — kept for `--use-pg-schema`, which wraps the in-process `stripe/pg-schema-diff` Go library with no TS/container equivalent (CLI-1960) -- `db branch create`, `db branch delete`, `db branch list`, `db branch switch` -- `db remote changes` -- `gen keys` — the public command is kept (its planned removal, CLI-1964, was cancelled as a breaking change); the Go implementation stays only until a native TS replacement lands -- `functions download` — kept for the hidden `--legacy-bundle` path (CLI-1963) - -That list is exhaustive: `GoProxy` is the only code path in the TypeScript CLI that spawns `supabase-go`. The one historical exception — the hidden pg-delta seam (`db schema declarative __catalog` + `db start`), spawned directly by the native `db schema declarative generate|sync` commands to provision shadow databases and export pg-delta catalogs — was ported to native TypeScript as part of CLI-1970 (`pgdelta.seam.layer.ts` now composes `setupShadowDatabase`/`legacyExportCatalogPgDelta`/`startLocalDatabase` in-process), and those two Go commands were deleted with it. - -Everything else the original Go CLI implemented was deleted outright from `apps/cli-go/` (CLI-1970), not just excluded from the shipped binary. The reachable set was computed with a `go list -deps -test` fixpoint from the trimmed `main` package, and everything outside it was removed: the main module's first-party package count went from 138 to 38 (100 packages / ~29.5k LOC across 321 files deleted), including every other command's `cmd/*.go` file, `internal/{inspect,storage,sso,login,link,init,bootstrap}`, `internal/migration/{squash,up,fetch}`, the Go docs generator (`docs/`), `examples/`, and `tools/{jsonschema,shared}`. Counting stdlib and third-party dependencies too, the full dependency closure shrank from 1078 to 959 packages. `pkg/` (a separate, independently tagged/published Go module for external consumers) and `tools/listdep` (used by `cli-go-mirror.yml`) were left untouched. - -For any command or package deleted by CLI-1970, the parity/provenance reference is the last commit with the full source intact: `7b469f5b3` — the same convention CLI-1966 established for `internal/start` (see below). - -### Binary size - -Measured on the CLI-1970 branch with the real release build (`build.ts`, `go build -trimpath -ldflags "-s -w"`, `CGO_ENABLED=0`): - -| Platform | `supabase-go` size | -| ------------- | -----------------: | -| darwin-arm64 | 40.1 MB | -| darwin-x64 | 42.6 MB | -| linux-arm64 | 39.1 MB | -| linux-x64 | 41.8 MB | -| windows-arm64 | 39.5 MB | -| windows-x64 | 42.8 MB | - -(musl packages ship the glibc `supabase-go` binary — see Package Layout above.) - -Progression across both trims: the original two-binary baseline was 97–103 MB per platform; CLI-1966's `internal/start` deletion cut it to roughly 47–52 MB; CLI-1970 brings it to 39.1–42.8 MB. - -The remaining size is dominated by dependencies the retained commands still need: `stripe/pg-schema-diff` (`db diff --use-pg-schema`), the Docker client (shadow-database provisioning for diff), pgx, the Management API client, cobra/viper, and sentry/posthog. - -Release archive sizes (TS `supabase` binary + `supabase-go` together): `.tar.gz` 37.9–52.9 MB, `.zip` (Windows) 50.0–53.0 MB, `.deb` 52.7–53.4 MB, `.rpm` 52.3–53.2 MB, `.apk` 52.8–54.1 MB. - -### Historical note: CLI-1966 (`internal/start` deletion) - -`apps/cli-go/internal/start` (Go's `supabase start` implementation) was the first package deleted outright rather than excluded from the build. Native TS `start` talks to Docker directly and never proxied to Go for it, and a repo-wide grep confirmed no other TS→Go delegation seam called into it either. `internal/start` alone had previously accounted for roughly half the shipped Go binary's size via its exclusive dependency tree (docker-compose/v2, buildx, buildkit, k8s client-go, aws-sdk-go-v2, notary, secret-detector). The last commit with the source intact is `a253ccba2` (full hash `a253ccba25c21356ccd33044c4474aecb77d1ae4`). +2. Signs the macOS binaries before archiving, so every channel ships the signed bytes — see [release-process.md § Code signing (macOS)](./release-process.md#code-signing-macos) and [ADR 0014](../../../docs/adr/0014-macos-code-signing-and-notarization.md) +3. Bundles the binary into the platform archives (`.tar.gz` / `.zip`) +4. Includes the binary in the Linux package manager packages (deb/rpm/apk) ## See Also diff --git a/apps/cli/docs/command-target.md b/apps/cli/docs/command-target.md new file mode 100644 index 0000000000..e1f502897e --- /dev/null +++ b/apps/cli/docs/command-target.md @@ -0,0 +1,149 @@ +# Command target + +Every command that acts on a database or hosted project announces the target it resolved, once, before it acts. The same identity goes to machine consumers as the JSON `target` object. + +This page is the shared reference for how a command's target is chosen, which errors the selection raises, the deprecated selector flags, how a hosted write is confirmed, and what the banner and the JSON `target` carry. [ADR 0033](../../../docs/adr/0033-target-selection-and-hosted-mutation-consent.md) records why. The `SIDE_EFFECTS.md` of every `hosted`, `selectable` and `account` command opens with a `Target:` line that links here and lists only what is specific to that command: its own calls, files, exit codes and prompt, and any selector rule that differs from this page. A `local-only` command has no `Target:` line. + +## Groups + +The `Target:` line names the command's group and its consent default: `Target: , consent default `, or `Target: , no consent` when the command never asks for hosted-mutation consent. A document that covers several subcommands gives each one's default. + +| Group | Acts on | With no `--remote` | Selector flags | +| ------------ | ------------------------------- | ------------------ | ---------------------------------------------------------------------------- | +| `selectable` | a database or the Storage API | the local stack | `--remote`; deprecated `--local`, `--linked`, `--project-ref` and `--db-url` | +| `hosted` | a hosted project | the linked project | `--remote=`; deprecated `--project-ref` | +| `account` | the account or an organisation | no project | none | +| `local-only` | the local stack or the checkout | the local stack | none | + +The consent default is the answer a confirmation starts on. It is `No` for anything that replaces or deletes live state and for arbitrary SQL, migration, seed and storage writes, and `Yes` for create-only operations (`projects create`, `orgs create`, `branches create`, `domains create` and `bootstrap`). `no consent` means the command never asks for hosted-mutation consent because it only reads or writes local files. It can still ask a local question of its own, such as `config pull` before it rewrites `config.toml` or `migration fetch` before it replaces local migrations. + +## Choosing a target + +| You pass | Target | +| ------------------- | ------------------------------------------------------------------------------- | +| nothing | The local stack on a `selectable` command; the linked project on a `hosted` one | +| `--remote` | The linked project | +| `--remote=` | That project: 20 lowercase letters, never looked up as a branch | +| `--remote=` | That database: a `postgres://` or `postgresql://` URL | +| `--remote=` | A branch of the linked project, by name or UUID | + +- **The linked project** is `SUPABASE_PROJECT_ID`, else `supabase/.temp/project-ref`. With neither, an interactive terminal (stdin and stdout are TTYs and the output is text) gets the project picker (`GET /v1/projects`), and any other run fails with `ProjectRefNotLinkedError`. A malformed ref from either source fails with `InvalidProjectRefError`. `SUPABASE_PROJECT_ID` alone never makes a `selectable` command remote: it is a ref source once `--remote` is passed, and enough for the `default; pass --remote` hint. The CLI never falls back between local and remote. +- **A branch** resolves with the lookup `supabase link` uses (`GET /v1/projects/{parent}/branches`, where `{parent}` is `SUPABASE_PROJECT_ID`, else the linked project), and the command acts on that branch's own project. With no linked project a UUID is looked up with `GET /v1/branches/{id}`, and one the API can't look up fails as ``Branch "" not found. Run `supabase branches list` to see available branches.``, never as a schema error. +- **Hosted-only commands** take `--remote=` for another project, and a bare `--remote` changes nothing for them. `link` takes its target as an argument and has no `--remote`. `branches *` commands act on a parent project, so their `--remote` takes a ref, never a branch name. +- **A database URL** applies only where the remote side is Postgres. `storage`, `seed buckets` and hosted commands need a project, so they reject `--remote=` when the flags are parsed, as an invalid `--remote` value (`--remote takes a project ref or a branch name.`, or `--remote takes a project ref.` on `branches *`). The run exits 1 before anything else happens, and the URL is never echoed. A value with `://` that is not a `postgres://` or `postgresql://` URL, and a keyword connection string (`host=… user=…`), is rejected on every command and is never looked up as a branch. +- **`--remote=true|yes|on|1|y`** means a bare `--remote`. `--no-remote` and `--remote=false|no|off|0|n` fail with `--remote doesn't take false. Leave it out to .`, and an empty `--remote=` with `--remote= has an empty value. Pass --remote for the linked project or --remote=; leave it out to .` `` is `target the local database`, `target the local Storage API` or, on a hosted command, `use the linked project`; `` is `` where the command takes a database URL, `` elsewhere and `` on `branches *`. Neither error prints the command's help. +- **A local stack that is down in a linked checkout.** A database command that defaults to the local stack and can't reach it adds `To use the linked project instead, pass --remote.` to its error's suggestion when the checkout is linked (`SUPABASE_PROJECT_ID` is set, or `supabase/.temp/project-ref` names a project). `gen types` adds it to its errors for a local database container that is missing or can't be inspected. + +## `--remote ` + +`--remote=` always parses as a value. A bare `--remote` followed by a word is rewritten to `--remote=` when the command takes no operands (a hidden one does not count), when the word is a database URL, or when the command already has more words than operands. + +The one ambiguous shape is a bare `--remote` directly followed by a word the command took as an operand. It is settled before anything runs, and the error offers both readings: + +- **A project-ref-shaped word** (20 lowercase letters) fails when the command still has room for operands, with no network call: pass `--remote=` for that project, or put `--remote` last to keep the word as an operand. It is read as `--remote=` only when the command takes no operands or its operands are already full. +- **Any other word** is checked against the branches of the linked project, when one is known. The run fails when the word names a branch, equals a branch name ignoring case, or is one edit from the name of a non-default branch of five or more characters; swapping two adjacent letters is one edit. The error offers `--remote=` for the branch, or the command's operands followed by `--remote` for the linked project. Otherwise the word runs as the operand. `--remote=` is separate: its "Did you mean" suggestion allows up to two edits. +- **A listing that fails** still fails the run before anything runs, and the error gives the cause: `HTTP `, the network error, or a missing or invalid access token. It never echoes the word, and its linked-project example reads `supabase --remote`. The suggestion follows the cause: a `401` says to log in again, or to fix `SUPABASE_ACCESS_TOKEN` when the token came from the environment; a `403` says to use a token that can read the project's branches; a `404` says the linked ref may be a preview branch, so link its parent project. +- **`branches *` commands** apply only the project-ref-shaped check: their `--remote` never names a branch, so any other word is the operand and nothing more is checked. + +## Selector errors + +These fail with exit 1 before the command does any work: + +| Condition | Error | +| --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | +| more than one of `--remote`, `--linked`, `--db-url` and `--local`, or `--project-ref` with `--project-id` | `TargetSelectorConflictError` | +| `--remote` given more than once with different values | `TargetSelectorConflictError` | +| `--project-ref` or `--project-id` with `--local`, `--db-url` or `--remote=` | `TargetProjectRefConflictError` | +| an empty `--db-url`, `--project-ref` or `--project-id` | `TargetEmptyValueError` | +| `--project-ref` or `--project-id` given a URL or connection string | `TargetInvalidValueError` | +| `--remote=` on a command with no database connection | a flag-parse error: an invalid `--remote` value | +| a branch name given to a `branches *` command | `TargetNeedsProjectError` | +| a bare `--remote` followed by a project-ref-shaped word, or by a word that may name a branch | `TargetRemoteAmbiguousError` | +| `--password` with `--remote=` or the local default | `TargetPasswordConflictError` | +| `--remote=` naming no branch of the linked project | `LinkBranchNotFoundError` | +| a branch name with no linked project to search | `LinkBranchNotLinkedError` | +| linked state whose ref is malformed, so there is no project to search | `LinkParentRefInvalidError` | +| a branch that has no project ref yet because it is still provisioning | `LinkBranchNotReadyError` | +| the branch lookup fails to reach the API or gets a non-200 status | `LinkBranchListNetworkError`, `LinkBranchListStatusError` | + +A selector conflict lists only the selector flags the command has: `Only one of --remote, --linked, --db-url and --local can be set; got …`, with the suggestion to use `--remote` or `--remote=` on its own. A bare `--remote` together with `--project-ref=` is accepted: the alias's value wins and its warning prints. A `false` switch counts as absent, so `--linked=false` selects nothing. A repeated `--remote` is one selection when every occurrence resolves to the same target, and a bare `--remote` is the linked project, so `--remote --remote` and `--remote= --remote=` run. When the occurrences differ the run fails before anything runs, with `--remote is given more than once with different values; pass it once.` and the suggestion to pass `--remote` once. + +`LinkBranchNotFoundError` names the parent project, lists the branches it has, offers the nearest name as "Did you mean" and, for a word of lowercase letters, or a 20-letter word with an uppercase letter (which is still looked up as a branch and gets the lowercase spelling as a suggestion), notes that a project ref is exactly 20 lowercase letters. A `404` from the branch listing adds that the linked ref may itself be a preview branch and says to link its parent project. + +## `--password` + +`db dump`, `db pull`, `db push`, `db schema declarative generate`, `migration list`, `migration repair` and `migration squash` take `--password`, which applies only when the target is a project: a bare `--remote`, a ref or a branch. Anything else fails before the command runs, with `TargetPasswordConflictError`: + +- `--remote=`: `--password can't be used with --remote=. Put the password in the connection string: postgres://USER:PASSWORD@HOST:PORT/postgres` +- the local default: `--password only applies to a linked project. The local database uses [db].password from supabase/config.toml.`, followed by `To use the linked project instead, pass --remote.` in a linked checkout. + +A command's own `SIDE_EFFECTS.md` records where else its password can come from and when it is withheld from a target. + +## Deprecated selector flags + +`--linked`, `--project-ref`, `--db-url` and `--local` (and `gen types --project-id`) still work in v3 and are removed in v4, as is the `linked` value of `db diff --from` and `--to`, which `remote` replaces. Each goes through the same resolver as the `--remote` form it maps to and prints a warning on stderr in every output format, before the `Target:` line. The warning names only the replacement: + +| Flag | The warning says | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--linked` | `--linked is deprecated and will be removed in v4. Use --remote instead` | +| `--project-ref=` | `--project-ref is deprecated and will be removed in v4. Use --remote= instead`, or `Use --remote= instead` when the value is a branch name | +| `--db-url=` | `--db-url is deprecated and will be removed in v4. Use --remote= instead`, then `, or --remote= with an access token set` when the URL names a Supabase project | +| `--local` | `--local isn't needed (local is the default) and will be removed in v4` | + +A warning never repeats an operand, a flag value or a database URL, because a CI log would keep them, and it carries no example command. Two values have their own wording: a branch whose name is a boolean literal (`true`, `false`, `yes`, `no`, `on`, `off`, `y`, `n`, `1`, `0`) is reached with `--remote=`, and a keyword `--db-url` connection string is converted to a `postgresql://` URL for `--remote=`. + +`db diff --from`/`--to` accepts the same flags and warns the same way, but names the `--from`/`--to` value that replaces each: `Use --from=remote or --to=remote instead` for `--linked`, `Use --from= or --to= instead` for `--project-ref` (`` for a branch name) and `Use --from= or --to= instead` for `--db-url`. `--project-ref` also points a `remote` side, or the deprecated `linked`, at the project it names, and fails when neither side is `remote`; `--linked`, `--db-url` and `--local` change nothing. `--remote` stays an error there. + +## Hosted-mutation consent + +Every persistent mutation of a hosted resource asks for consent first, including a write through a non-local `--remote=`. A loopback host on the local or shadow database ports, or the local stack's endpoint, counts as local. + +- **Approval.** `--yes`, or a `SUPABASE_YES` in the shell environment. A `SUPABASE_YES` in a project `.env*` file does not count. The last `--yes` on the command line wins, and `SUPABASE_YES` is read only when `--yes` never appears, so `--yes=false --yes` confirms and `--yes --yes=false` declines. A bare `--yes` followed by `false`, `no`, `off`, `0` or `n` reads as false, as the flag parser does. Stdin is never read, so a piped `y` does nothing. The confirmation line names which one approved: `Confirmed by --yes: on `. +- **Prompt.** Only text output on an interactive stdout with a TTY stdin, with no agent driving, is asked. The question starts on the command's consent default. +- **Refusal.** JSON, stream-json, `-o json|yaml|toml|env`, agent and non-TTY runs fail with `HostedMutationConsentRequiredError` unless `--yes` is set. The error names the action and target, says why the run can't ask and tells an agent to ask the user before passing `--yes`. Nothing is changed. +- **Decline.** A declined prompt fails with `HostedMutationDeclinedError` (`context canceled`), exit 1, and nothing is changed. +- **One question.** A command asks once, after the plan is known and before the first persistent effect, and lists everything it will do. +- **Network bans.** Removing a network ban to connect needs `--yes` or a shell `SUPABASE_YES`; without one, the command fails with `LinkedDbNetworkBannedError`. Its suggestion is `Re-run with --yes to remove them, or run supabase network-bans remove --db-unban-ip --experimental --yes --remote=`, and `` is always the project whose bans blocked the connection, which after `db pull --remote=` is not the linked project. This applies to reads too. +- **Reads never prompt.** A command marked `no consent` never asks for hosted-mutation consent, on any target, and `--yes` and `SUPABASE_YES` have no effect on it. `db query --read-only` is a read and skips consent too. + +## Banner + +`Target: ` on stderr in every output format, printed once per distinct text. It is best effort: it adds no API call and no auth requirement, and it never prints credentials. + +| Target | Text | +| ------------------------------------- | ----------------------------------------------------------------------------------- | +| Local stack | `local stack (:)` | +| Local stack, nothing selected, linked | `local stack (:) · default; pass --remote for ()` | +| `--remote=` | `custom Postgres (:)`; the connection string's credentials never appear | +| Linked project the cache names | `project () · production` | +| Linked project nothing vouches for | `project `, or `project ()` when the project picker named it | +| Ref other than the linked project | `project (not the linked project)` | +| Branch found by name or UUID | `project () · branch ()` | +| The project's default branch | `project () · production` | +| Branch UUID, no linked project | `project `: the lookup does not say whether it is the default branch | + +On a `hosted` command the banner is the notice that the command is talking to a remote project, and it names the linked project the command defaults to. The `default; pass --remote` hint appears only when a project is linked, by `SUPABASE_PROJECT_ID` or the link file, and no `--remote` was passed; a bare `db push`, `db pull` or `migration fetch` in a linked checkout runs locally and prints it. + +Identity comes from `supabase/.temp/linked-project.json`, with no API call. A cache that names the resolved ref itself marks a production project. The cache can be stale or missing, so it never proves a branch: "branch" is claimed only on positive evidence, which is a branch lookup by name or UUID or a live probe. A branch's parent shows as its bare ref when the cache doesn't know its name. + +## JSON `target` + +`json` and `stream-json` results carry `target`, tagged by `kind`: + +| `kind` | Fields | +| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `local` | `host`, `port` | +| `db-url` | `host`, `port` | +| `linked` | `project_ref`, `environment` (`production`, `branch` or `unverified`), optional `project_name`, optional `parent_project_ref` (a branch's parent project) | + +`environment` is `production` when the linked-project cache names the resolved ref or the resolved branch is the project's default branch, `branch` on positive evidence of any other branch, and `unverified` otherwise, including a branch UUID looked up with no linked project. A failed identity check is `unverified`, never `branch`. + +The first announced target becomes the run's `target`, so a command that announces two (`db diff --from=remote --to=`) prints both and reports the first. `db diff --from`/`--to` announces every database endpoint in resolution order, the local stack and a database URL included, so `--from local` reports `{"kind":"local","host":"127.0.0.1","port":54322}`; `migrations` announces nothing, and the same target on both sides prints one line. It is merged into object results, the `stream-json` result event and the machine error envelope. Arrays, `--output {json,yaml,toml,env}` payloads and generated artifacts such as `gen types` output are unchanged. A command that owns extra `target` fields (for example `branch`, `local_scope`, or `config push`'s `environment`) adds them in front of the announced identity's, and its own values win on a clash. + +The schema is `CommandTargetJson` in `src/shared/output/command-target.service.ts`; every target is encoded through it before it reaches output. + +## Help and docs + +A group's help labels each subcommand `[local or remote]` (`selectable`), `[remote only]` (`hosted`) or `[local only]`; `account` and removed commands carry no label. A command's own help has a `TARGET` section above `USAGE`, and its `--remote` row reads `--remote[=]` where the command takes a database URL, `--remote[=]` where it doesn't, and `--remote[=]` on `branches *`. JSON help gives the `remote` flag `value` and `valueOptional` fields in place of the bracketed name. + +Docs and examples put `--remote` last, in the `=` form when it has a value (`db pull add_profiles --remote`, `db push --remote=`), so it cannot be read as anything else. Shell completion never offers a space-separated value for `--remote` on a command that takes operands. diff --git a/apps/cli/docs/compute-commands.md b/apps/cli/docs/compute-commands.md index f69581da9b..ae8b3a4395 100644 --- a/apps/cli/docs/compute-commands.md +++ b/apps/cli/docs/compute-commands.md @@ -6,8 +6,9 @@ configuration may change incompatibly while experimental. The family is opt in. `SUPABASE_EXPERIMENTAL_COMPUTE=1` or by setting `compute = true` under `[experimental]` in `supabase/config.toml`. -The environment variable accepts `1` to enable and `0` to disable. When it is -unset or empty, the config file is used; any other non-empty value reports an +The environment variable accepts a boolean (`1`, `t`, `T`, `TRUE`, `true`, `True` / `0`, `f`, `F`, `FALSE`, `false`, `False`; +other casings such as `tRue` fail) to enable or disable the family. When it is unset or empty, the config file is used; any +non-boolean value reports an invalid feature-flag value on Compute paths (including their completion), root help, or root-level completion before a subcommand is chosen. Unrelated commands do not resolve the Compute flag. Unreadable or malformed @@ -46,3 +47,9 @@ settings into an ignored file. To deploy a source directory without a Compute configuration entry, `compute push` requires an explicit `--exposure public` or `--exposure private`. Record the entry in the authoritative config to persist its deployment settings. + +`compute push` and `compute delete` change the remote project, so both ask for +confirmation first, and both default to no. `compute delete` asks for the +compute's name to be typed back. Pass `--yes`, or set `SUPABASE_YES` in your +shell, to confirm in scripts and CI; without it, a run that cannot prompt fails +before changing anything. diff --git a/apps/cli/docs/go-cli-divergences.md b/apps/cli/docs/go-cli-divergences.md deleted file mode 100644 index 54ef768b90..0000000000 --- a/apps/cli/docs/go-cli-divergences.md +++ /dev/null @@ -1,255 +0,0 @@ -# Go CLI Divergences - -**Frozen historical ledger** of deliberate TypeScript divergences from the old Go CLI -(pre-`7b469f5b3`) on the CLI: TS-only commands, flags, and behavior with no Go -counterpart. The TypeScript CLI is now the source of truth, so this ledger no longer accumulates -entries — new flags, commands, and behavioral changes are simply new CLI behavior, documented -through help text, tests, and each command's `SIDE_EFFECTS.md`. This document exists to answer -support and migration questions about why the TS CLI does something the old Go CLI didn't — it is -not a compatibility promise. - -## TS-only Commands - -These commands exist in the TS CLI today but have no direct top-level equivalent in the old Go CLI reference. - -| TS command | TS path | Notes | -| ---------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `dev` | `planned` | Reserved for a TS-native long-running local development workflow command that watches files and orchestrates subcommands. Track this as TS-only unless a direct Go equivalent emerges. | - -## Flag divergences from the Go reference - -- `db diff`, `db pull`, and `db schema declarative generate`/`sync` have a TS-only - `--strict-coverage` flag (no Go equivalent). It applies whenever the bundled - pg-delta engine runs (always for the declarative commands; for `db diff` and - migration-style `db pull` when pg-delta is selected via - `[experimental.pgdelta] enabled = true`, `--use-pg-delta`, or - `--diff-engine pg-delta`): coverage gaps that the engine reports — statements - it skipped or objects it could not represent — normally surface as warnings, - and `--strict-coverage` promotes them to hard failures. Selecting migra (the - `db diff` / migration-style `db pull` default, or explicitly via `--use-migra` - / `--diff-engine migra`) accepts the flag but has no effect, since migra does - not emit coverage diagnostics. Default behavior (omitted flag) matches Go. -- `db push` has a TS-only `--skip-vault` flag. It applies migrations without - resolving or updating `[db.vault]` secrets; default behavior still matches Go. -- Every command that resolves a linked project ref for its own database - connection has a TS-only `--project-ref` flag (no Go equivalent on any - user-facing command — only the `SUPABASE_PROJECT_ID` env var could override - the linked ref; the Go registrations are a hidden, non-user-facing seam, - `db declarative __catalog --project-ref`, `cmd/pgdelta_catalog.go:44`, and the - deprecated Go-delegated `db remote changes`, which receives the linked ref this - way in stack mode). This - covers: `db push`, `db pull`, `db diff`, `db dump`, `db reset`, `db lint`, - `db advisors`, `db query`; `migration list`/`up`/`down`/`repair`/`fetch`/`squash`; - `seed buckets`; `storage ls`/`cp`/`mv`/`rm`; every `inspect db` subcommand and - `inspect report`; and `test db` (and its hidden `db test` alias, which shares - `test db`'s flag config verbatim). It feeds - `RemoteTarget.load`, keeping Go's precedence (flag > - `SUPABASE_PROJECT_ID` > `supabase/.temp/project-ref`) and taking effect only on - the linked path. It shares ONLY that ref-resolution precedence with - `SUPABASE_PROJECT_ID` — unlike the env var, it does not affect the local - container id or the pg-delta project id, and it does not imply `--linked`: - passing it alongside `--local`/`--db-url` (or, for `db diff`, in explicit mode - without `--from`/`--to linked`) is a hard error rather than a silently ignored - flag, since Go's env var going unused on a non-linked target has no TS-only - flag counterpart to accidentally discard. Default behavior (omitted flag) - matches Go exactly. -- A hosted ref from `SUPABASE_PROJECT_ID` that differs from the linked ref gets - its pooler URL from the Management API, like a `--project-ref` flag does. Go - reuses the saved `supabase/.temp/pooler-url` whichever ref the env var names. - The saved pooler URL, parent project, and branch name apply only when the - resolved ref is the linked one. -- `projects api-keys` has a TS-only `--reveal` flag (no Go equivalent). It sends - `reveal=true` so the Management API returns the full secret keys (`sb_secret_...`) in - full instead of redacting them, addressing issue #4775. Default behavior (omitted flag) - matches Go exactly. -- `projects create` has a TS-only `--high-availability` flag (no Go equivalent). It sets - `high_availability` in the create request body. Default behavior (omitted flag) matches - Go exactly. -- `projects create` has TS-only `--release-channel` and `--postgres-engine` flags (no Go - equivalent). They set `release_channel` / `postgres_engine` in the create request body — - fields the upstream Management API OpenAPI spec deliberately hides even though - `POST /v1/projects` accepts them (restored via `packages/api/scripts/openapi-overrides.json`, - CLI-2180). Both flags are hidden and gated behind `--experimental` (or `SUPABASE_EXPERIMENTAL`) - until PROD-548 exposes them officially; omitting them matches Go exactly. -- `db schema declarative generate` has a TS-only `--output-dir ` flag (no Go equivalent, - no short alias). It writes the generated declarative tree to the given directory for this - invocation only, without changing the configured `declarative_schema_path` — the staging step - of the legacy-tree upgrade recipe printed by the sync/generate compatibility gates. The name - deliberately avoids `--output`/`-o`, which the root command reserves for the global - machine-format flag; a leaf string flag would shadow it and turn `generate -o json` into a - write to a directory named `json`. Default behavior (omitted flag) matches Go. -- `link` has a TS-only `[ref-or-branch]` positional argument (no Go equivalent), and its - `--project-ref` flag additionally accepts a branch name. A value matching the 20-lowercase-letter - project ref shape is always treated as a ref; any other non-empty value is resolved to its - parent-project's branch project ref via the Management API before linking proceeds exactly as - today (CLI-2167). On the 404 (branch) link path, `link` also best-effort maintains - `linked-project.json`'s PARENT evidence (PR #6168 review, TS-only, no Go equivalent — Go never - writes this cache for a branch ref at all): a name/UUID-resolved branch link persists its known - parent ref (a ref-only record when no richer cache exists yet); a raw ref-shaped branch link - whose existing cache names a different project best-effort correlates the two via one extra - `listAllBranches` call and deletes the cache on EVERY unverified result — a verified mismatch, - a timeout, or any transport/status/decode failure (fail-safe: an unverifiable divergent cache is - untrustworthy). Both are best-effort and never affect `link`'s own outcome — see - `link/SIDE_EFFECTS.md`. -- `link` has a TS-only `--frozen` flag (no Go equivalent) that leaves `supabase/supabase.lock` - untouched in stack mode; it is a no-op in legacy mode, which has no lock. - -## Behavioral divergences from the Go reference - -- `db schema declarative generate`/`sync` default declarative directory is `supabase/schemas`; - the old Go CLI reference (pre-`7b469f5b3`) used `supabase/database`. The move aligns the - default with the product-wide declarative-schemas convention. To keep the upgrade visible, - both commands print a TS-only warning when `declarative_schema_path` is unset, the new - default directory is empty, and the former `supabase/database` default still contains `.sql` - files or an export manifest — telling the user to set - `declarative_schema_path = "./database"` or move the tree. The warning never changes - behavior or exit codes; a non-interactive sync still fails with Go's - "no declarative schema found" message. Inside that directory the bundled pg-delta - engine writes one directory per schema at the root — `supabase/schemas/public/tables/x.sql` — - with cluster-level objects under a reserved `supabase/schemas/_cluster/`. - Structured export is TypeScript-only; the Go binary no longer ships a - pg-delta dump path. -- Local `pg_net` presence now converges with `[experimental.webhooks]` instead of being - installed unconditionally: `db-webhook.sql` no longer creates the extension at container - init, `supabase start`/`db start` install it (with grants reapplied via the - `issue_pg_net_access` event trigger) only when webhooks are enabled or migration history - contains a `create extension … pg_net`, and an existing-volume `start` with webhooks - disabled drops a `pg_net` that migration history does not own. Known accepted edge: `pg_net` - installed OUTSIDE migrations (for example via local Studio's SQL editor or extension toggle) - is invisible to the migration-ownership heuristic and is dropped on the next `start`; a - tracked dependency on `net.*` (PG14+ `BEGIN ATOMIC` functions) makes that non-`CASCADE` drop - — and therefore `start` — fail. Workaround in both cases: enable `[experimental.webhooks]` - or declare the extension in a migration. Documented rather than special-cased. -- `test db` (and its `db test` alias) exits `1` when `pg_prove` ran no tests (CLI-2194, #6206). - `pg_prove` prints `Result: NOTESTS` and still exits `0` for an empty run, and Go returns that - code verbatim (`internal/db/test/test.go` → `DockerRunOnceWithConfig`), so a typo'd path, an - empty tests directory, or a bind the daemon resolved against a different filesystem than the - CLI's (a sibling-container Docker socket) all reported a green build that ran zero tests. The - TAP stream on stdout is unchanged; the diagnostic goes to stderr like every other failure. -- SQL file runners on a stepped-down session re-assert `SET SESSION ROLE postgres` - immediately after each top-level role revert (`RESET ROLE`, the generic-`SET` spellings - such as `SET ROLE [TO|=] NONE|DEFAULT` and a case-sensitively quoted `'none'`, - `RESET SESSION AUTHORIZATION`, `SET SESSION AUTHORIZATION DEFAULT`, `DISCARD ALL`), - at the end of each file, and before the migration history insert and the `seed_files` - upsert — so the whole file (including a post-reset `granted by current_user` cleanup), - every CLI-owned ledger write, and every subsequent file run as `postgres`, matching a - password session for `current_user` and privilege checks (CLI-2205, #6236). - `session_user` remains the login role and `current_setting('role')` reads `postgres` - rather than `none`, so a file keying on `session_user` still diverges. A stepped-down - session is any remote connection authenticating as `cli_login_*` (the passwordless - linked path) or `supabase_admin`, which steps down with a session-level - `SET SESSION ROLE postgres`; a file's own `RESET ROLE` reverted it to the login role, - so the appended history insert failed with SQLSTATE 42501 and any later file ran as - the login role. The old Go CLI had the same defect; on a `supabase_admin` `--db-url`, - `RESET ROLE` consequently no longer re-escalates to superuser mid-file. - Statement-level re-assertion is used because a connection-time `role=postgres` default - cannot be guaranteed through the pooler. Injected restores never shift - `At statement: N` and are never recorded in the history row. Residual: a role revert - issued through dynamic SQL, or a spelling outside the list above (for example - `SET LOCAL ROLE NONE` — deliberately unmatched, a session-scoped restore would - override its transaction scope — or the `session_authorization` GUC spellings), is - invisible to the lexical check, so statements after it run as the login role until - the next restore point; the end-of-file restore still protects every CLI-owned write. - (`RESET ALL` needs no entry — `role` carries `GUC_NO_RESET_ALL`.) -- `functions serve` per-function env discovery (CLI-2184, #6179): without `--env-file`, each - `supabase/functions//.env` overrides matching values from the shared - `supabase/functions/.env` for that Function only; an explicit `--env-file` remains the - highest-priority source and disables both automatic reads. TS-only feature — the old Go - command read only the shared fallback. -- `functions deploy`/`functions serve` import-map resolution follows the import-maps spec - (implemented by Deno): a key matches exactly, or as a prefix only when it ends with `/`. The - old Go walker prefix-matched every key (`pkg/function/deno.go:150-155`, still in-tree), which - fabricated paths the runtime could never resolve — the ENOTDIR crash family fixed in - PR #6164 (CLI-2179). Intentional divergence: the spec behavior is the correct one. -- `config push`/`start` auth email `content_path` resolution: every relative - `[auth.email.template.*]` AND `[auth.email.notification.*]` path resolves from the discovered - project root, with notifications additionally falling back to the legacy `supabase/`-relative - location when the root-resolved file is missing. Go resolves notifications from `supabase/` only - (`(*baseConfig).resolve`'s own `// FIXME`-flagged asymmetry). Config validation, `config push` - content loading, and Kong's template mount builder all share one resolver - (`resolveNotificationContentPath`), so every consumer reads the SAME file — the drift the - asymmetry caused (validated against `/supabase/...`, mounted from `/...`) is gone. - The `init` scaffold ejects the root-relative form, which is incompatible with Go if uncommented - (#6159/#6160). -- `db remote changes --password

`: since CLI-1970, an explicit - `--password` beats the `SUPABASE_DB_PASSWORD` env var. Before the trim, Go's - package-wide "last `viper.BindPFlag("DB_PASSWORD", …)` wins" behavior bound - the key to `projects create --db-password` (lexically last `cmd/*.go` file), - so `db remote`'s own `--password` flag was never the bound instance and env - silently won over it — a latent bug. With `projects.go` deleted, the bind - lands on `db remote`'s persistent `--password` and flag-beats-env applies as - intended. Accepted (not restored) in the CLI-1970 parity audit. `db remote -commit` is now native `db pull`, so it uses pull's flag-then-env-then-dotenv - password order. `db pull` keeps that precedence unchanged. -- `branches {list,create,get,update,delete,pause,unpause,disable}` resolve their project ref - through a PARENT-scoped chain instead of plain `--project-ref` flag/env/file resolution: an - explicit `--project-ref` still wins outright, but the fallback is env `SUPABASE_PROJECT_ID` → - `supabase/.temp/linked-project.json`'s `ref` → `supabase/.temp/project-ref`, first ref-shaped - candidate wins. This is a direct consequence of the `link` branch-name divergence above: after - `supabase link `, `project-ref` holds the branch's own ref, and the Management API - returns 403 for a branch ref on every branches-management endpoint. No-op when linked to a real - (non-branch) project — the cache and the file hold the same ref — so this only changes behavior - in the previously-403ing branch-linked state (CLI-2167 follow-up, no Go equivalent). -- `branches list`'s pretty table (not `-o json|yaml|toml`, not `--output-format json|stream-json`) - marks the row matching the CURRENTLY linked ref with a ` (active)` NAME cell. TS-only - QoL, no Go equivalent (CLI-2167 follow-up). -- `status` prints the current linked project/branch as a "Linked Project:" block on stdout in - human text mode (Neon-style — `Org:`/`Project:`/`Branch:` lines, each omitted when unknown), - before any daemon/stack work begins, and folds the same linked state into its machine-readable - outputs — additive `linked_project: {...} | null` (with `org_slug`/`org_id`) in the TS - `--output-format json`/`stream-json` payload, and additive `linked_project_ref`/ - `linked_project_name`/`linked_org_slug`/`linked_org_id`/`linked_branch`/ - `linked_parent_project_ref` keys (absent entirely when not linked) appended after the existing - keys in `-o env|json|yaml|toml`. A confirmed branch-linked state (from `linked-project.json`) - keeps showing the parent/org fields even when the branch-name lookup itself degrades (no - token, offline, API error) — only the branch's own name is ever missing, so the user always - sees they're on a branch. The Management API client for that lookup is acquired lazily - (`CommandPlatformApiFactory`, not the eager `CommandPlatformApi`) so `status` stays fully - functional offline/token-less. Intent: let an agent driving `status` discover which - project/branch it's on without a separate `link`/`branches` call. Read-only, never affects - `status`'s exit code, and never alters any of its existing failure behavior — a Docker/daemon - connection failure still fails exactly as today, with the linked block already printed above it - in text mode (CLI-2167 follow-up, no Go equivalent). The same `linked_project` object is also - carried on the `--output-format json`/`stream-json` FAILURE envelope (top-level, next to - `_tag`/`error` or `type`/`error`/`timestamp`) — the agent-discovery use case matters most when - `status` fails to reach a stopped stack — via a new opt-in, shared mechanism - (`shared/output/machine-error-context.service.ts`'s `MachineErrorContext`, read by - `jsonOutputLayer`/`streamJsonOutputLayer`'s `fail`); `-o env|json|yaml|toml`'s failure output is - deliberately unchanged (still no payload, matching Go). See `status/SIDE_EFFECTS.md`. -- `projects list`'s `LINKED` marker (the `linked` boolean, rendered as the pretty table's `●` - bullet) now falls back to the PARENT chain when the linked ref matches no row exactly — the same - scenario as above, since a branch-linked ref never matches a real project row. **This changes - the `linked` field in the `-o json|yaml|toml` Go-struct payloads too**: in the branch-linked - state it was previously `false` on every row; it can now be `true` on the parent project's row. - The truthful fix IS the behavior change (CLI-2167 follow-up, no Go equivalent). -- `services` warns on a malformed linked project ref (matching Go's - `flags.LoadProjectRef` validation message) but, unlike Go, does not then use - that ref for the remote lookup. Go's `cmd/services.go` (deleted in CLI-1970; - last present at commit 7b469f5b3) treats the validation failure as - non-fatal and still calls `listRemoteImages` with the malformed - value; TS skips the remote lookup instead, since the ref is embedded - unescaped into the tenant gateway hostname and a malformed value could - redirect the service-role key to an attacker-controlled host. Intentional - TS-only hardening, not a parity bug — see - [`services/SIDE_EFFECTS.md`](../src/commands/services/SIDE_EFFECTS.md). -- `db pull` in-sync (`"No schema changes found"`) keeps Go's message and its non-zero - exit code, but replaces the generic "Try rerunning the command with --debug to - troubleshoot the error." stderr footer with an explanatory suggestion line - ("The remote database is already in sync with your local migrations — nothing to - pull."). An in-sync database is a finding, not a failure to troubleshoot, so the - debug hint sent users chasing a non-existent bug. Message text and exit code — the - parts scripts depend on — are unchanged. -- Edge Runtime's Docker container `--ulimit nofile` value (`functions serve` and `start`): Go - hardcodes `nofile=65536:65536`, raised from the daemon default to accommodate FD usage from - many concurrent Deno isolates (supabase/cli#5151). TS clamps that value to the host's own hard - nofile limit on Linux (`@supabase/stack`'s `edgeRuntimeNofileUlimit`, via - `process.report`'s `userLimits`), so a constrained sandbox (hard cap below 65536) can still start the - container instead of failing outright (CLI-2220). The CLI process's own limit is used as a - proxy for the daemon's — exact in the sandboxes this targets, where both share the cap; a - Linux client more constrained than its daemon (remote `DOCKER_HOST`, mounted socket) just - gets a smaller fd budget, never a failed start. When the clamp lowers the request, the CLI's - `functions serve`/`start` bring-up warns with the reduced limit. The `@supabase/stack` service - builder (`stack start`) applies the same clamp silently: its defs are built without - an output channel, and in managed mode inside the daemon process, so a user-visible warning - there needs a diagnostics channel on `BuildResult` first; the applied value stays visible via - `docker inspect`. diff --git a/apps/cli/docs/go-cli-porting-status.md b/apps/cli/docs/go-cli-porting-status.md deleted file mode 100644 index 5e37434a43..0000000000 --- a/apps/cli/docs/go-cli-porting-status.md +++ /dev/null @@ -1,66 +0,0 @@ -# Go CLI Delegation - -> Still named `go-cli-porting-status.md` for historical reasons — the per-command porting tracker -> this file once held is gone now that the port is done; it documents only the residual Go -> delegation surface below. - -The Go→TypeScript port is complete (CLI-1970). The bundled `supabase-go` binary and the -`apps/cli-go/` tree contain **only** the delegation surface in the table below — Go source for -every other command was deleted outright once nothing in the TypeScript CLI could reach it, -directly or indirectly. For any other command's former Go source, the reference is the last commit -with it intact: `7b469f5b3` (CLI-1966's `internal/start` pin remains its own, separate commit, -`a253ccba2`). - -See [`binary-distribution.md`](./binary-distribution.md) for how these two binaries are packaged, -resolved at runtime, and sized. The TypeScript CLI is the source of truth for all CLI behavior; -`apps/cli-go/` is authoritative only for the proxied commands below, and the whole surface is -slated for cleanup and removal ([ADR 0016](../../../docs/adr/0016-port-completion-and-go-cli-authority-scope.md) -records the earlier transition policy). - -## The delegation surface - -| Command/path | TS proxy site | Go implementation (in-tree) | Why it stays | -| -------------------------------------------------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| `db diff --use-pg-schema` | [`src/commands/db/diff/diff.handler.ts`](../src/commands/db/diff/diff.handler.ts) (delegate path) | `internal/db/diff/pgschema.go` | Wraps the Go-only `stripe/pg-schema-diff` library (CLI-1960); deprecated but sanctioned. | -| `db branch create\|delete\|list\|switch` | `src/commands/db/branch/*/` | `legacy/branch/*` | Go-deprecated wrapped commands kept indefinitely (CLI-1964 cancelled: dropping them was ruled a breaking change not worth shipping). | -| `db remote changes` | `src/commands/db/remote/changes/` | inline in `cmd/db.go` over `internal/db/diff` | Same CLI-1964 ruling. `db remote commit` is native `db pull`. | -| `gen keys` | `src/commands/gen/keys/` | `legacy/keys` | Same ruling; requires `--experimental`. | -| `functions download --legacy-bundle` (hidden flag) | `src/shared/functions/download.ts` `makeGoProxyLegacyBundleArgs` | `internal/functions/download` | Legacy Deno bundle extraction (CLI-1963). | - -## Mechanics - -All delegation goes through the shared `GoProxy` service -([`src/command-internal/go-proxy.service.ts`](../src/command-internal/go-proxy.service.ts), -[`go-proxy.layer.ts`](../src/command-internal/go-proxy.layer.ts)): - -- **Binary resolution order**: `SUPABASE_GO_BINARY` env var → binary co-located with the compiled - shim → the platform's `@supabase/cli-` npm package. There is deliberately **no** - `PATH` fallback (CLI-1488: the shim itself is what's on `PATH`, so falling back would re-invoke - it and fork-bomb) — resolution failure is a hard error with install guidance. PATH-installed - setups work via the co-location step: `supabase-go` sits next to the `supabase` shim. -- **Global-flag forwarding**: `cli/root.ts` translates the CLI's global flags - (`--output`, `--profile`, `--debug`, `--workdir`, `--experimental`, `--network-id`, `--yes`, - `--dns-resolver`, `--create-ticket`, `--agent`) into Go-style argv ahead of every proxied - invocation. -- **Child telemetry suppression**: a proxy handler that itself owns the parent - `cli_command_executed` event (wrapped in TS command instrumentation) suppresses the child's own - telemetry; a bare pass-through proxy leaves it enabled so the Go child stays the sole emitter. -- **Stack-mode state**: when the project keeps its state outside `supabase/.temp`, `GoProxy` sets - `SUPABASE_INTERNAL_STACK_STATE=1` so the child skips `supabase/.temp`, and passes the linked - project from `supabase/.link.json` as the JSON of `linked-project.json` in - `SUPABASE_INTERNAL_LINKED_PROJECT` so the child keeps its telemetry groups. A command that - passes `--project-ref` also passes the resolved target's `pooler_url` in - `SUPABASE_INTERNAL_POOLER_URL` when that ref is the linked one; the child reads it in place of - `supabase/.temp/pooler-url` for its IPv4 pooler fallback. - -The spawn surface is guarded by -[`apps/cli-e2e/src/tests/go-binary-surface.e2e.test.ts`](../../cli-e2e/src/tests/go-binary-surface.e2e.test.ts) — -trimming the binary past this surface fails CI. - -The former pg-delta `__catalog`/`db start` seam (spawned directly by `db schema declarative -generate|sync`, outside `GoProxy`) was ported native in CLI-1970 and no longer exists. - -## See also - -TS-only flags and deliberate behavioral divergences from the old Go CLI live in -[`go-cli-divergences.md`](./go-cli-divergences.md). diff --git a/apps/cli/docs/go-cli-reference.md b/apps/cli/docs/go-cli-reference.md deleted file mode 100644 index e557eefadf..0000000000 --- a/apps/cli/docs/go-cli-reference.md +++ /dev/null @@ -1,2128 +0,0 @@ -# Old Go CLI Reference - -> Complete help output for the old Go-based `supabase` CLI. -> Use this document as the raw parity reference for the residual Go delegation surface in [`go-cli-porting-status.md`](./go-cli-porting-status.md) and the TS divergences tracked in [`go-cli-divergences.md`](./go-cli-divergences.md). - -## Global Flags - -These flags are available on all commands: - -``` -Flags: - --create-ticket create a support ticket for any CLI error - --debug output debug logs to stderr - --dns-resolver [ native | https ] lookup domain names using the specified resolver (default native) - --experimental enable experimental features - -h, --help help for supabase - --network-id string use the specified docker network instead of a generated one - -o, --output [ env | pretty | json | toml | yaml ] output format of status variables (default pretty) - --profile string use a specific profile for connecting to Supabase API (default "supabase") - --workdir string path to a Supabase project directory - --yes answer yes to all prompts -``` - -## Table of Contents - -- [Quick Start](#quick-start) - - [bootstrap](#bootstrap) -- [Local Development](#local-development) - - [init](#init) - - [link](#link) - - [unlink](#unlink) - - [login](#login) - - [logout](#logout) - - [start](#start) - - [stop](#stop) - - [status](#status) - - [services](#services) - - [db](#db) - - [gen](#gen) - - [inspect](#inspect) - - [migration](#migration) - - [seed](#seed) - - [test](#test) -- [Management APIs](#management-apis) - - [backups](#backups) - - [branches](#branches) - - [config](#config) - - [domains](#domains) - - [encryption](#encryption) - - [functions](#functions) - - [network-bans](#network-bans) - - [network-restrictions](#network-restrictions) - - [orgs](#orgs) - - [postgres-config](#postgres-config) - - [projects](#projects) - - [secrets](#secrets) - - [snippets](#snippets) - - [ssl-enforcement](#ssl-enforcement) - - [sso](#sso) - - [storage](#storage) - - [vanity-subdomains](#vanity-subdomains) -- [Additional Commands](#additional-commands) - - [completion](#completion) - - [help](#help-1) - ---- - -## Quick Start - -### bootstrap - -``` -Bootstrap a Supabase project from a starter template - -Usage: - supabase bootstrap [template] [flags] - -Flags: - -h, --help help for bootstrap - -p, --password string Password to your remote Postgres database. -``` - ---- - -## Local Development - -### init - -``` -Initialize a local project - -Usage: - supabase init [flags] - -Flags: - --force Overwrite existing supabase/config.toml. - -h, --help help for init - -i, --interactive Enables interactive mode to configure IDE settings. - --use-orioledb Use OrioleDB storage engine for Postgres. -``` - -### link - -``` -Link to a Supabase project - -Usage: - supabase link [flags] - -Flags: - -h, --help help for link - -p, --password string Password to your remote Postgres database. - --project-ref string Project ref of the Supabase project. - --skip-pooler Use direct connection instead of pooler. -``` - -### unlink - -``` -Unlink a Supabase project - -Usage: - supabase unlink [flags] - -Flags: - -h, --help help for unlink -``` - -### login - -``` -Authenticate using an access token - -Usage: - supabase login [flags] - -Flags: - -h, --help help for login - --name string Name that will be used to store token in your settings (default "built-in token name generator") - --no-browser Do not open browser automatically - --token string Use provided token instead of automatic login flow -``` - -### logout - -``` -Log out and delete access tokens locally - -Usage: - supabase logout [flags] - -Flags: - -h, --help help for logout -``` - -### start - -``` -Start containers for Supabase local development - -Usage: - supabase start [flags] - -Flags: - -x, --exclude strings Names of containers to not start. [gotrue,realtime,storage-api,imgproxy,kong,mailpit,postgrest,postgres-meta,studio,edge-runtime,logflare,vector,supavisor] - -h, --help help for start - --ignore-health-check Ignore unhealthy services and exit 0 - --sandbox Run in sandbox mode using native binaries (experimental) -``` - -### stop - -``` -Stop all local Supabase containers - -Usage: - supabase stop [flags] - -Flags: - --all Stop all local Supabase instances from all projects across the machine. - -h, --help help for stop - --no-backup Deletes all data volumes after stopping. - --project-id string Local project ID to stop. -``` - -### status - -``` -Show status of local Supabase containers - -Usage: - supabase status [flags] - -Examples: - supabase status -o env --override-name api.url=NEXT_PUBLIC_SUPABASE_URL - supabase status -o json - -Flags: - -h, --help help for status - --override-name strings Override specific variable names. -``` - -### services - -``` -Show versions of all Supabase services - -Usage: - supabase services [flags] - -Flags: - -h, --help help for services -``` - -### db - -``` -Manage Postgres databases - -Usage: - supabase db [command] - -Available Commands: - diff Diffs the local database for schema changes - dump Dumps data or schemas from the remote database - lint Checks local database for typing error - pull Pull schema from the remote database - push Push new migrations to the remote database - reset Resets the local database to current migrations - start Starts local Postgres database - -Flags: - -h, --help help for db -``` - -#### db diff - -``` -Diffs the local database for schema changes - -Usage: - supabase db diff [flags] - -Flags: - --db-url string Diffs against the database specified by the connection string (must be percent-encoded). - -f, --file string Saves schema diff to a new migration file. - -h, --help help for diff - --linked Diffs local migration files against the linked project. - --local Diffs local migration files against the local database. (default true) - -s, --schema strings Comma separated list of schema to include. - --use-migra Use migra to generate schema diff. (default true) - --use-pg-delta Use pg-delta to generate schema diff. - --use-pg-schema Use pg-schema-diff to generate schema diff. - --use-pgadmin Use pgAdmin to generate schema diff. -``` - -#### db dump - -``` -Dumps data or schemas from the remote database - -Usage: - supabase db dump [flags] - -Flags: - --data-only Dumps only data records. - --db-url string Dumps from the database specified by the connection string (must be percent-encoded). - --dry-run Prints the pg_dump script that would be executed. - -x, --exclude strings List of schema.tables to exclude from data-only dump. - -f, --file string File path to save the dumped contents. - -h, --help help for dump - --keep-comments Keeps commented lines from pg_dump output. - --linked Dumps from the linked project. (default true) - --local Dumps from the local database. - -p, --password string Password to your remote Postgres database. - --role-only Dumps only cluster roles. - -s, --schema strings Comma separated list of schema to include. - --use-copy Use copy statements in place of inserts. -``` - -#### db lint - -``` -Checks local database for typing error - -Usage: - supabase db lint [flags] - -Flags: - --db-url string Lints the database specified by the connection string (must be percent-encoded). - --fail-on [ none | warning | error ] Error level to exit with non-zero status. (default none) - -h, --help help for lint - --level [ warning | error ] Error level to emit. (default warning) - --linked Lints the linked project for schema errors. - --local Lints the local database for schema errors. (default true) - -s, --schema strings Comma separated list of schema to include. -``` - -#### db pull - -``` -Pull schema from the remote database - -Usage: - supabase db pull [migration name] [flags] - -Flags: - --db-url string Pulls from the database specified by the connection string (must be percent-encoded). - -h, --help help for pull - --linked Pulls from the linked project. (default true) - --local Pulls from the local database. - -p, --password string Password to your remote Postgres database. - -s, --schema strings Comma separated list of schema to include. -``` - -#### db push - -``` -Push new migrations to the remote database - -Usage: - supabase db push [flags] - -Flags: - --db-url string Pushes to the database specified by the connection string (must be percent-encoded). - --dry-run Print the migrations that would be applied, but don't actually apply them. - -h, --help help for push - --include-all Include all migrations not found on remote history table. - --include-roles Include custom roles from supabase/roles.sql. - --include-seed Include seed data from your config. - --linked Pushes to the linked project. (default true) - --local Pushes to the local database. - -p, --password string Password to your remote Postgres database. -``` - -#### db reset - -``` -Resets the local database to current migrations - -Usage: - supabase db reset [flags] - -Flags: - --db-url string Resets the database specified by the connection string (must be percent-encoded). - -h, --help help for reset - --last uint Reset up to the last n migration versions. - --linked Resets the linked project with local migrations. - --local Resets the local database with local migrations. (default true) - --no-seed Skip running the seed script after reset. - --version string Reset up to the specified version. -``` - -#### db start - -``` -Starts local Postgres database - -Usage: - supabase db start [flags] - -Flags: - --from-backup string Path to a logical backup file. - -h, --help help for start -``` - -### gen - -``` -Run code generation tools - -Usage: - supabase gen [command] - -Available Commands: - bearer-jwt Generate a Bearer Auth JWT for accessing Data API - signing-key Generate a JWT signing key - types Generate types from Postgres schema - -Flags: - -h, --help help for gen -``` - -#### gen bearer-jwt - -``` -Generate a Bearer Auth JWT for accessing Data API - -Usage: - supabase gen bearer-jwt [flags] - -Flags: - --exp time Expiry timestamp for this token. - -h, --help help for bearer-jwt - --payload string Custom claims in JSON format. (default "{}") - --role string Postgres role to use. - --sub string User ID to impersonate. (default "anonymous") - --valid-for duration Validity duration for this token. (default 30m0s) -``` - -#### gen signing-key - -``` -Securely generate a private JWT signing key for use in the CLI or to import in the dashboard. - -Supported algorithms: - ES256 - ECDSA with P-256 curve and SHA-256 (recommended) - RS256 - RSA with SHA-256 - -Usage: - supabase gen signing-key [flags] - -Flags: - --algorithm [ RS256 | ES256 ] Algorithm for signing key generation. (default ES256) - --append Append new key to existing keys file instead of overwriting. - -h, --help help for signing-key -``` - -#### gen types - -``` -Generate types from Postgres schema - -Usage: - supabase gen types [flags] - -Examples: - supabase gen types --local - supabase gen types --linked --lang=go - supabase gen types --project-id abc-def-123 --schema public --schema private - supabase gen types --db-url 'postgresql://...' --schema public --schema auth - -Flags: - --db-url string Generate types from a database url. - -h, --help help for types - --lang [ typescript | go | swift | python ] Output language of the generated types. (default typescript) - --linked Generate types from the linked project. - --local Generate types from the local dev database. - --postgrest-v9-compat Generate types compatible with PostgREST v9 and below. - --project-id string Generate types from a project ID. - --query-timeout duration Maximum timeout allowed for the database query. (default 15s) - -s, --schema strings Comma separated list of schema to include. - --swift-access-control [ internal | public ] Access control for Swift generated types. (default internal) -``` - -### inspect - -``` -Tools to inspect your Supabase project - -Usage: - supabase inspect [command] - -Available Commands: - db Tools to inspect your Supabase database - report Generate a CSV output for all inspect commands - -Flags: - --db-url string Inspect the database specified by the connection string (must be percent-encoded). - -h, --help help for inspect - --linked Inspect the linked project. (default true) - --local Inspect the local database. -``` - -#### inspect db - -``` -Tools to inspect your Supabase database - -Usage: - supabase inspect db [command] - -Available Commands: - bloat Estimates space allocated to a relation that is full of dead tuples - blocking Show queries that are holding locks and the queries that are waiting for them to be released - calls Show queries from pg_stat_statements ordered by total times called - db-stats Show stats such as cache hit rates, total sizes, and WAL size - index-stats Show combined index size, usage percent, scan counts, and unused status - locks Show queries which have taken out an exclusive lock on a relation - long-running-queries Show currently running queries running for longer than 5 minutes - outliers Show queries from pg_stat_statements ordered by total execution time - replication-slots Show information about replication slots on the database - role-stats Show information about roles on the database - table-stats Show combined table size, index size, and estimated row count - traffic-profile Show read/write activity ratio for tables based on block I/O operations - vacuum-stats Show statistics related to vacuum operations per table - -Flags: - -h, --help help for db -``` - -##### inspect db bloat - -``` -Estimates space allocated to a relation that is full of dead tuples - -Usage: - supabase inspect db bloat [flags] - -Flags: - -h, --help help for bloat -``` - -##### inspect db blocking - -``` -Show queries that are holding locks and the queries that are waiting for them to be released - -Usage: - supabase inspect db blocking [flags] - -Flags: - -h, --help help for blocking -``` - -##### inspect db calls - -``` -Show queries from pg_stat_statements ordered by total times called - -Usage: - supabase inspect db calls [flags] - -Flags: - -h, --help help for calls -``` - -##### inspect db db-stats - -``` -Show stats such as cache hit rates, total sizes, and WAL size - -Usage: - supabase inspect db db-stats [flags] - -Flags: - -h, --help help for db-stats -``` - -##### inspect db index-stats - -``` -Show combined index size, usage percent, scan counts, and unused status - -Usage: - supabase inspect db index-stats [flags] - -Flags: - -h, --help help for index-stats -``` - -##### inspect db locks - -``` -Show queries which have taken out an exclusive lock on a relation - -Usage: - supabase inspect db locks [flags] - -Flags: - -h, --help help for locks -``` - -##### inspect db long-running-queries - -``` -Show currently running queries running for longer than 5 minutes - -Usage: - supabase inspect db long-running-queries [flags] - -Flags: - -h, --help help for long-running-queries -``` - -##### inspect db outliers - -``` -Show queries from pg_stat_statements ordered by total execution time - -Usage: - supabase inspect db outliers [flags] - -Flags: - -h, --help help for outliers -``` - -##### inspect db replication-slots - -``` -Show information about replication slots on the database - -Usage: - supabase inspect db replication-slots [flags] - -Flags: - -h, --help help for replication-slots -``` - -##### inspect db role-stats - -``` -Show information about roles on the database - -Usage: - supabase inspect db role-stats [flags] - -Flags: - -h, --help help for role-stats -``` - -##### inspect db table-stats - -``` -Show combined table size, index size, and estimated row count - -Usage: - supabase inspect db table-stats [flags] - -Flags: - -h, --help help for table-stats -``` - -##### inspect db traffic-profile - -``` -Show read/write activity ratio for tables based on block I/O operations - -Usage: - supabase inspect db traffic-profile [flags] - -Flags: - -h, --help help for traffic-profile -``` - -##### inspect db vacuum-stats - -``` -Show statistics related to vacuum operations per table - -Usage: - supabase inspect db vacuum-stats [flags] - -Flags: - -h, --help help for vacuum-stats -``` - -#### inspect report - -``` -Generate a CSV output for all inspect commands - -Usage: - supabase inspect report [flags] - -Flags: - -h, --help help for report - --output-dir string Path to save CSV files in (default ".") -``` - -### migration - -``` -Manage database migration scripts - -Usage: - supabase migration [command] - -Aliases: - migration, migrations - -Available Commands: - down Resets applied migrations up to the last n versions - fetch Fetch migration files from history table - list List local and remote migrations - new Create an empty migration script - repair Repair the migration history table - squash Squash migrations to a single file - up Apply pending migrations to local database - -Flags: - -h, --help help for migration -``` - -#### migration down - -``` -Resets applied migrations up to the last n versions - -Usage: - supabase migration down [flags] - -Flags: - --db-url string Resets applied migrations on the database specified by the connection string (must be percent-encoded). - -h, --help help for down - --last uint Reset up to the last n migration versions. (default 1) - --linked Resets applied migrations on the linked project. - --local Resets applied migrations on the local database. (default true) -``` - -#### migration fetch - -``` -Fetch migration files from history table - -Usage: - supabase migration fetch [flags] - -Flags: - --db-url string Fetches migrations from the database specified by the connection string (must be percent-encoded). - -h, --help help for fetch - --linked Fetches migration history from the linked project. (default true) - --local Fetches migration history from the local database. -``` - -#### migration list - -``` -List local and remote migrations - -Usage: - supabase migration list [flags] - -Flags: - --db-url string Lists migrations of the database specified by the connection string (must be percent-encoded). - -h, --help help for list - --linked Lists migrations applied to the linked project. (default true) - --local Lists migrations applied to the local database. - -p, --password string Password to your remote Postgres database. -``` - -#### migration new - -``` -Create an empty migration script - -Usage: - supabase migration new [flags] - -Flags: - -h, --help help for new -``` - -#### migration repair - -``` -Repair the migration history table - -Usage: - supabase migration repair [version] ... [flags] - -Flags: - --db-url string Repairs migrations of the database specified by the connection string (must be percent-encoded). - -h, --help help for repair - --linked Repairs the migration history of the linked project. (default true) - --local Repairs the migration history of the local database. - -p, --password string Password to your remote Postgres database. - --status [ applied | reverted ] Version status to update. -``` - -#### migration squash - -``` -Squash migrations to a single file - -Usage: - supabase migration squash [flags] - -Flags: - --db-url string Squashes migrations of the database specified by the connection string (must be percent-encoded). - -h, --help help for squash - --linked Squashes the migration history of the linked project. - --local Squashes the migration history of the local database. (default true) - -p, --password string Password to your remote Postgres database. - --version string Squash up to the specified version. -``` - -#### migration up - -``` -Apply pending migrations to local database - -Usage: - supabase migration up [flags] - -Flags: - --db-url string Applies migrations to the database specified by the connection string (must be percent-encoded). - -h, --help help for up - --include-all Include all migrations not found on remote history table. - --linked Applies pending migrations to the linked project. - --local Applies pending migrations to the local database. (default true) -``` - -### seed - -``` -Seed a Supabase project from supabase/config.toml - -Usage: - supabase seed [command] - -Available Commands: - buckets Seed buckets declared in [storage.buckets] - -Flags: - -h, --help help for seed - --linked Seeds the linked project. - --local Seeds the local database. (default true) -``` - -#### seed buckets - -``` -Seed buckets declared in [storage.buckets] - -Usage: - supabase seed buckets [flags] - -Flags: - -h, --help help for buckets -``` - -### test - -``` -Run tests on local Supabase containers - -Usage: - supabase test [command] - -Available Commands: - db Tests local database with pgTAP - new Create a new test file - -Flags: - -h, --help help for test -``` - -#### test db - -``` -Tests local database with pgTAP - -Usage: - supabase test db [path] ... [flags] - -Flags: - --db-url string Tests the database specified by the connection string (must be percent-encoded). - -h, --help help for db - --linked Runs pgTAP tests on the linked project. - --local Runs pgTAP tests on the local database. (default true) -``` - -#### test new - -``` -Create a new test file - -Usage: - supabase test new [flags] - -Flags: - -h, --help help for new - -t, --template [ pgtap ] Template framework to generate. (default pgtap) -``` - ---- - -## Management APIs - -### backups - -``` -Manage Supabase physical backups - -Usage: - supabase backups [command] - -Available Commands: - list Lists available physical backups - restore Restore to a specific timestamp using PITR - -Flags: - -h, --help help for backups - --project-ref string Project ref of the Supabase project. -``` - -#### backups list - -``` -Lists available physical backups - -Usage: - supabase backups list [flags] - -Flags: - -h, --help help for list -``` - -#### backups restore - -``` -Restore to a specific timestamp using PITR - -Usage: - supabase backups restore [flags] - -Flags: - -h, --help help for restore - -t, --timestamp int The recovery time target in seconds since epoch. -``` - -### branches - -``` -Manage Supabase preview branches - -Usage: - supabase branches [command] - -Available Commands: - create Create a preview branch - delete Delete a preview branch - get Retrieve details of a preview branch - list List all preview branches - pause Pause a preview branch - unpause Unpause a preview branch - update Update a preview branch - -Flags: - -h, --help help for branches - --project-ref string Project ref of the Supabase project. -``` - -#### branches create - -``` -Create a preview branch for the linked project. - -Usage: - supabase branches create [name] [flags] - -Flags: - -h, --help help for create - --notify-url string URL to notify when branch is active healthy. - --persistent Whether to create a persistent branch. - --region string Select a region to deploy the branch database. - --size string Select a desired instance size for the branch database. - --with-data Whether to clone production data to the branch database. -``` - -#### branches delete - -``` -Delete a preview branch by its name or ID. - -Usage: - supabase branches delete [name] [flags] - -Flags: - -h, --help help for delete -``` - -#### branches get - -``` -Retrieve details of the specified preview branch. - -Usage: - supabase branches get [name] [flags] - -Flags: - -h, --help help for get -``` - -#### branches list - -``` -List all preview branches of the linked project. - -Usage: - supabase branches list [flags] - -Flags: - -h, --help help for list -``` - -#### branches pause - -``` -Pause a preview branch - -Usage: - supabase branches pause [name] [flags] - -Flags: - -h, --help help for pause -``` - -#### branches unpause - -``` -Unpause a preview branch - -Usage: - supabase branches unpause [name] [flags] - -Flags: - -h, --help help for unpause -``` - -#### branches update - -``` -Update a preview branch by its name or ID. - -Usage: - supabase branches update [name] [flags] - -Flags: - --git-branch string Change the associated git branch. - -h, --help help for update - --name string Rename the preview branch. - --notify-url string URL to notify when branch is active healthy. - --persistent Switch between ephemeral and persistent branch. - --status string Override the current branch status. -``` - -### config - -``` -Manage Supabase project configurations - -Usage: - supabase config [command] - -Available Commands: - push Pushes local config.toml to the linked project - -Flags: - -h, --help help for config - --project-ref string Project ref of the Supabase project. -``` - -#### config push - -``` -Pushes local config.toml to the linked project - -Usage: - supabase config push [flags] - -Flags: - -h, --help help for push -``` - -### domains - -``` -Manage custom domain names for Supabase projects. - -Use of custom domains and vanity subdomains is mutually exclusive. - -Usage: - supabase domains [command] - -Available Commands: - activate Activate the custom hostname for a project - create Create a custom hostname - delete Deletes the custom hostname config for your project - get Get the current custom hostname config - reverify Re-verify the custom hostname config for your project - -Flags: - -h, --help help for domains - --include-raw-output Include raw output (useful for debugging). - --project-ref string Project ref of the Supabase project. -``` - -#### domains activate - -``` -Activates the custom hostname configuration for a project. - -This reconfigures your Supabase project to respond to requests on your custom hostname. -After the custom hostname is activated, your project's auth services will no longer function on the Supabase-provisioned subdomain. - -Usage: - supabase domains activate [flags] - -Flags: - -h, --help help for activate -``` - -#### domains create - -``` -Create a custom hostname for your Supabase project. - -Expects your custom hostname to have a CNAME record to your Supabase project's subdomain. - -Usage: - supabase domains create [flags] - -Flags: - --custom-hostname string The custom hostname to use for your Supabase project. - -h, --help help for create -``` - -#### domains delete - -``` -Deletes the custom hostname config for your project - -Usage: - supabase domains delete [flags] - -Flags: - -h, --help help for delete -``` - -#### domains get - -``` -Retrieve the custom hostname config for your project, as stored in the Supabase platform. - -Usage: - supabase domains get [flags] - -Flags: - -h, --help help for get -``` - -#### domains reverify - -``` -Re-verify the custom hostname config for your project - -Usage: - supabase domains reverify [flags] - -Flags: - -h, --help help for reverify -``` - -### encryption - -``` -Manage encryption keys of Supabase projects - -Usage: - supabase encryption [command] - -Available Commands: - get-root-key Get the root encryption key of a Supabase project - update-root-key Update root encryption key of a Supabase project - -Flags: - -h, --help help for encryption - --project-ref string Project ref of the Supabase project. -``` - -#### encryption get-root-key - -``` -Get the root encryption key of a Supabase project - -Usage: - supabase encryption get-root-key [flags] - -Flags: - -h, --help help for get-root-key -``` - -#### encryption update-root-key - -``` -Update root encryption key of a Supabase project - -Usage: - supabase encryption update-root-key [flags] - -Flags: - -h, --help help for update-root-key -``` - -### functions - -``` -Manage Supabase Edge functions - -Usage: - supabase functions [command] - -Available Commands: - delete Delete a Function from Supabase - deploy Deploy a Function to Supabase - download Download a Function from Supabase - list List all Functions in Supabase - new Create a new Function locally - serve Serve all Functions locally - -Flags: - -h, --help help for functions -``` - -#### functions delete - -``` -Delete a Function from the linked Supabase project. This does NOT remove the Function locally. - -Usage: - supabase functions delete [flags] - -Flags: - -h, --help help for delete - --project-ref string Project ref of the Supabase project. -``` - -#### functions deploy - -``` -Deploy a Function to the linked Supabase project. - -Usage: - supabase functions deploy [Function name] [flags] - -Flags: - -h, --help help for deploy - --import-map string Path to import map file. - -j, --jobs uint Maximum number of parallel jobs. (default 1) - --no-verify-jwt Disable JWT verification for the Function. - --project-ref string Project ref of the Supabase project. - --prune Delete Functions that exist in Supabase project but not locally. - --use-api Bundle functions server-side without using Docker. -``` - -#### functions download - -``` -Download the source code for a Function from the linked Supabase project. If no function name is provided, downloads all functions. - -Usage: - supabase functions download [Function name] [flags] - -Flags: - -h, --help help for download - --project-ref string Project ref of the Supabase project. - --use-api Unbundle functions server-side without using Docker. -``` - -#### functions list - -``` -List all Functions in the linked Supabase project. - -Usage: - supabase functions list [flags] - -Flags: - -h, --help help for list - --project-ref string Project ref of the Supabase project. -``` - -#### functions new - -``` -Create a new Function locally - -Usage: - supabase functions new [flags] - -Flags: - -h, --help help for new -``` - -#### functions serve - -``` -Serve all Functions locally - -Usage: - supabase functions serve [flags] - -Flags: - --env-file string Path to an env file to be populated to the Function environment. - -h, --help help for serve - --import-map string Path to import map file. - --inspect Alias of --inspect-mode brk. - --inspect-main Allow inspecting the main worker. - --inspect-mode [ run | brk | wait ] Activate inspector capability for debugging. - --no-verify-jwt Disable JWT verification for the Function. -``` - -### network-bans - -``` -Network bans are IPs that get temporarily blocked if their traffic pattern looks abusive (e.g. multiple failed auth attempts). - -The subcommands help you view the current bans, and unblock IPs if desired. - -Usage: - supabase network-bans [command] - -Available Commands: - get Get the current network bans - remove Remove a network ban - -Flags: - -h, --help help for network-bans - --project-ref string Project ref of the Supabase project. -``` - -#### network-bans get - -``` -Get the current network bans - -Usage: - supabase network-bans get [flags] - -Flags: - -h, --help help for get -``` - -#### network-bans remove - -``` -Remove a network ban - -Usage: - supabase network-bans remove [flags] - -Flags: - --db-unban-ip strings IP to allow DB connections from. - -h, --help help for remove -``` - -### network-restrictions - -``` -Manage network restrictions - -Usage: - supabase network-restrictions [command] - -Available Commands: - get Get the current network restrictions - update Update network restrictions - -Flags: - -h, --help help for network-restrictions - --project-ref string Project ref of the Supabase project. -``` - -#### network-restrictions get - -``` -Get the current network restrictions - -Usage: - supabase network-restrictions get [flags] - -Flags: - -h, --help help for get -``` - -#### network-restrictions update - -``` -Update network restrictions - -Usage: - supabase network-restrictions update [flags] - -Flags: - --append Append to existing restrictions instead of replacing them. - --bypass-cidr-checks Bypass some of the CIDR validation checks. - --db-allow-cidr strings CIDR to allow DB connections from. - -h, --help help for update -``` - -### orgs - -``` -Manage Supabase organizations - -Usage: - supabase orgs [command] - -Available Commands: - create Create an organization - list List all organizations - -Flags: - -h, --help help for orgs -``` - -#### orgs create - -``` -Create an organization for the logged-in user. - -Usage: - supabase orgs create [flags] - -Flags: - -h, --help help for create -``` - -#### orgs list - -``` -List all organizations the logged-in user belongs. - -Usage: - supabase orgs list [flags] - -Flags: - -h, --help help for list -``` - -### postgres-config - -``` -Manage Postgres database config - -Usage: - supabase postgres-config [command] - -Available Commands: - delete Delete specific Postgres database config overrides - get Get the current Postgres database config overrides - update Update Postgres database config - -Flags: - -h, --help help for postgres-config - --project-ref string Project ref of the Supabase project. -``` - -#### postgres-config delete - -``` -Delete specific config overrides, reverting them to their default values. - -Usage: - supabase postgres-config delete [flags] - -Flags: - --config strings Config keys to delete (comma-separated) - -h, --help help for delete - --no-restart Do not restart the database after deleting config. -``` - -#### postgres-config get - -``` -Get the current Postgres database config overrides - -Usage: - supabase postgres-config get [flags] - -Flags: - -h, --help help for get -``` - -#### postgres-config update - -``` -Overriding the default Postgres config could result in unstable database behavior. -Custom configuration also overrides the optimizations generated based on the compute add-ons in use. - -Usage: - supabase postgres-config update [flags] - -Flags: - --config strings Config overrides specified as a 'key=value' pair - -h, --help help for update - --no-restart Do not restart the database after updating config. - --replace-existing-overrides If true, replaces all existing overrides with the ones provided. If false (default), merges existing overrides with the ones provided. -``` - -### projects - -``` -Manage Supabase projects - -Usage: - supabase projects [command] - -Available Commands: - api-keys List all API keys for a Supabase project - create Create a project on Supabase - delete Delete a Supabase project - list List all Supabase projects - -Flags: - -h, --help help for projects -``` - -#### projects api-keys - -``` -List all API keys for a Supabase project - -Usage: - supabase projects api-keys [flags] - -Flags: - -h, --help help for api-keys - --project-ref string Project ref of the Supabase project. -``` - -#### projects create - -``` -Create a project on Supabase - -Usage: - supabase projects create [project name] [flags] - -Examples: -supabase projects create my-project --org-id cool-green-pqdr0qc --db-password ******** --region us-east-1 - -Flags: - --db-password string Database password of the project. - -h, --help help for create - --org-id string Organization ID to create the project in. - --region string Select a region close to you for the best performance. - --size string Select a desired instance size for your project. -``` - -#### projects delete - -``` -Delete a Supabase project - -Usage: - supabase projects delete [ref] [flags] - -Flags: - -h, --help help for delete -``` - -#### projects list - -``` -List all Supabase projects the logged-in user can access. - -Usage: - supabase projects list [flags] - -Flags: - -h, --help help for list -``` - -### secrets - -``` -Manage Supabase secrets - -Usage: - supabase secrets [command] - -Available Commands: - list List all secrets on Supabase - set Set a secret(s) on Supabase - unset Unset a secret(s) on Supabase - -Flags: - -h, --help help for secrets - --project-ref string Project ref of the Supabase project. -``` - -#### secrets list - -``` -List all secrets in the linked project. - -Usage: - supabase secrets list [flags] - -Flags: - -h, --help help for list -``` - -#### secrets set - -``` -Set a secret(s) to the linked Supabase project. - -Usage: - supabase secrets set ... [flags] - -Flags: - --env-file string Read secrets from a .env file. - -h, --help help for set -``` - -#### secrets unset - -``` -Unset a secret(s) from the linked Supabase project. - -Usage: - supabase secrets unset [NAME] ... [flags] - -Flags: - -h, --help help for unset -``` - -### snippets - -``` -Manage Supabase SQL snippets - -Usage: - supabase snippets [command] - -Available Commands: - download Download contents of a SQL snippet - list List all SQL snippets - -Flags: - -h, --help help for snippets - --project-ref string Project ref of the Supabase project. -``` - -#### snippets download - -``` -Download contents of the specified SQL snippet. - -Usage: - supabase snippets download [flags] - -Flags: - -h, --help help for download -``` - -#### snippets list - -``` -List all SQL snippets of the linked project. - -Usage: - supabase snippets list [flags] - -Flags: - -h, --help help for list -``` - -### ssl-enforcement - -``` -Manage SSL enforcement configuration - -Usage: - supabase ssl-enforcement [command] - -Available Commands: - get Get the current SSL enforcement configuration - update Update SSL enforcement configuration - -Flags: - -h, --help help for ssl-enforcement - --project-ref string Project ref of the Supabase project. -``` - -#### ssl-enforcement get - -``` -Get the current SSL enforcement configuration - -Usage: - supabase ssl-enforcement get [flags] - -Flags: - -h, --help help for get -``` - -#### ssl-enforcement update - -``` -Update SSL enforcement configuration - -Usage: - supabase ssl-enforcement update [flags] - -Flags: - --disable-db-ssl-enforcement Whether the DB should disable SSL enforcement for all external connections. - --enable-db-ssl-enforcement Whether the DB should enable SSL enforcement for all external connections. - -h, --help help for update -``` - -### sso - -``` -Manage Single Sign-On (SSO) authentication for projects - -Usage: - supabase sso [command] - -Available Commands: - add Add a new SSO identity provider - info Returns the SAML SSO settings required for the identity provider - list List all SSO identity providers for a project - remove Remove an existing SSO identity provider - show Show information about an SSO identity provider - update Update information about an SSO identity provider - -Flags: - -h, --help help for sso - --project-ref string Project ref of the Supabase project. -``` - -#### sso add - -``` -Add and configure a new connection to a SSO identity provider to your Supabase project. - -Usage: - supabase sso add [flags] - -Examples: - supabase sso add --type saml --project-ref mwjylndxudmiehsxhmmz --metadata-url 'https://...' --domains example.com - -Flags: - --attribute-mapping-file string File containing a JSON mapping between SAML attributes to custom JWT claims. - --domains strings Comma separated list of email domains to associate with the added identity provider. - -h, --help help for add - --metadata-file string File containing a SAML 2.0 Metadata XML document describing the identity provider. - --metadata-url string URL pointing to a SAML 2.0 Metadata XML document describing the identity provider. - --name-id-format string URI reference representing the classification of string-based identifier information. - --skip-url-validation Whether local validation of the SAML 2.0 Metadata URL should not be performed. - -t, --type [ saml ] Type of identity provider (according to supported protocol). -``` - -#### sso info - -``` -Returns all of the important SSO information necessary for your project to be registered with a SAML 2.0 compatible identity provider. - -Usage: - supabase sso info [flags] - -Examples: - supabase sso info --project-ref mwjylndxudmiehsxhmmz - -Flags: - -h, --help help for info -``` - -#### sso list - -``` -List all connections to a SSO identity provider to your Supabase project. - -Usage: - supabase sso list [flags] - -Examples: - supabase sso list --project-ref mwjylndxudmiehsxhmmz - -Flags: - -h, --help help for list -``` - -#### sso remove - -``` -Remove a connection to an already added SSO identity provider. Removing the provider will prevent existing users from logging in. Please treat this command with care. - -Usage: - supabase sso remove [flags] - -Examples: - supabase sso remove b5ae62f9-ef1d-4f11-a02b-731c8bbb11e8 --project-ref mwjylndxudmiehsxhmmz - -Flags: - -h, --help help for remove -``` - -#### sso show - -``` -Provides the information about an established connection to an identity provider. You can use --metadata to obtain the raw SAML 2.0 Metadata XML document stored in your project's configuration. - -Usage: - supabase sso show [flags] - -Examples: - supabase sso show b5ae62f9-ef1d-4f11-a02b-731c8bbb11e8 --project-ref mwjylndxudmiehsxhmmz - -Flags: - -h, --help help for show - --metadata Show SAML 2.0 XML Metadata only -``` - -#### sso update - -``` -Update the configuration settings of a already added SSO identity provider. - -Usage: - supabase sso update [flags] - -Examples: - supabase sso update b5ae62f9-ef1d-4f11-a02b-731c8bbb11e8 --project-ref mwjylndxudmiehsxhmmz --add-domains example.com - -Flags: - --add-domains strings Add this comma separated list of email domains to the identity provider. - --attribute-mapping-file string File containing a JSON mapping between SAML attributes to custom JWT claims. - --domains strings Replace domains with this comma separated list of email domains. - -h, --help help for update - --metadata-file string File containing a SAML 2.0 Metadata XML document describing the identity provider. - --metadata-url string URL pointing to a SAML 2.0 Metadata XML document describing the identity provider. - --name-id-format string URI reference representing the classification of string-based identifier information. - --remove-domains strings Remove this comma separated list of email domains from the identity provider. - --skip-url-validation Whether local validation of the SAML 2.0 Metadata URL should not be performed. -``` - -### storage - -``` -Manage Supabase Storage objects - -Usage: - supabase storage [command] - -Available Commands: - cp Copy objects from src to dst path - ls List objects by path prefix - mv Move objects from src to dst path - rm Remove objects by file path - -Flags: - -h, --help help for storage - --linked Connects to Storage API of the linked project. (default true) - --local Connects to Storage API of the local database. -``` - -#### storage cp - -``` -Copy objects from src to dst path - -Usage: - supabase storage cp [flags] - -Examples: -cp readme.md ss:///bucket/readme.md -cp -r docs ss:///bucket/docs -cp -r ss:///bucket/docs . -Flags: - --cache-control string Custom Cache-Control header for HTTP upload. (default "max-age=3600") - --content-type string Custom Content-Type header for HTTP upload. (default "auto-detect") - -h, --help help for cp - -j, --jobs uint Maximum number of parallel jobs. (default 1) - -r, --recursive Recursively copy a directory. -``` - -#### storage ls - -``` -List objects by path prefix - -Usage: - supabase storage ls [path] [flags] - -Examples: -ls ss:///bucket/docs - -Flags: - -h, --help help for ls - -r, --recursive Recursively list a directory. -``` - -#### storage mv - -``` -Move objects from src to dst path - -Usage: - supabase storage mv [flags] - -Examples: -mv -r ss:///bucket/docs ss:///bucket/www/docs - -Flags: - -h, --help help for mv - -r, --recursive Recursively move a directory. -``` - -#### storage rm - -``` -Remove objects by file path - -Usage: - supabase storage rm ... [flags] - -Examples: -rm -r ss:///bucket/docs -rm ss:///bucket/docs/example.md ss:///bucket/readme.md -Flags: - -h, --help help for rm - -r, --recursive Recursively remove a directory. -``` - -### vanity-subdomains - -``` -Manage vanity subdomains for Supabase projects. - -Usage of vanity subdomains and custom domains is mutually exclusive. - -Usage: - supabase vanity-subdomains [command] - -Available Commands: - activate Activate a vanity subdomain - check-availability Checks if a desired subdomain is available for use - delete Deletes a project's vanity subdomain - get Get the current vanity subdomain - -Flags: - -h, --help help for vanity-subdomains - --project-ref string Project ref of the Supabase project. -``` - -#### vanity-subdomains activate - -``` -Activate a vanity subdomain for your Supabase project. - -This reconfigures your Supabase project to respond to requests on your vanity subdomain. -After the vanity subdomain is activated, your project's auth services will no longer function on the {project-ref}.{supabase-domain} hostname. - -Usage: - supabase vanity-subdomains activate [flags] - -Flags: - --desired-subdomain string The desired vanity subdomain to use for your Supabase project. - -h, --help help for activate -``` - -#### vanity-subdomains check-availability - -``` -Checks if a desired subdomain is available for use - -Usage: - supabase vanity-subdomains check-availability [flags] - -Flags: - --desired-subdomain string The desired vanity subdomain to use for your Supabase project. - -h, --help help for check-availability -``` - -#### vanity-subdomains delete - -``` -Deletes the vanity subdomain for a project, and reverts to using the project ref for routing. - -Usage: - supabase vanity-subdomains delete [flags] - -Flags: - -h, --help help for delete -``` - -#### vanity-subdomains get - -``` -Get the current vanity subdomain - -Usage: - supabase vanity-subdomains get [flags] - -Flags: - -h, --help help for get -``` - ---- - -## Additional Commands - -### completion - -``` -Generate the autocompletion script for supabase for the specified shell. -See each sub-command's help for details on how to use the generated script. - -Usage: - supabase completion [command] - -Available Commands: - bash Generate the autocompletion script for bash - fish Generate the autocompletion script for fish - powershell Generate the autocompletion script for powershell - zsh Generate the autocompletion script for zsh - -Flags: - -h, --help help for completion -``` - -#### completion bash - -``` -Generate the autocompletion script for the bash shell. - -This script depends on the 'bash-completion' package. -If it is not installed already, you can install it via your OS's package manager. - -To load completions in your current shell session: - - source <(supabase completion bash) - -To load completions for every new session, execute once: - -#### Linux: - - supabase completion bash > /etc/bash_completion.d/supabase - -#### macOS: - - supabase completion bash > $(brew --prefix)/etc/bash_completion.d/supabase - -You will need to start a new shell for this setup to take effect. - -Usage: - supabase completion bash - -Flags: - -h, --help help for bash - --no-descriptions disable completion descriptions -``` - -#### completion fish - -``` -Generate the autocompletion script for the fish shell. - -To load completions in your current shell session: - - supabase completion fish | source - -To load completions for every new session, execute once: - - supabase completion fish > ~/.config/fish/completions/supabase.fish - -You will need to start a new shell for this setup to take effect. - -Usage: - supabase completion fish [flags] - -Flags: - -h, --help help for fish - --no-descriptions disable completion descriptions -``` - -#### completion powershell - -``` -Generate the autocompletion script for powershell. - -To load completions in your current shell session: - - supabase completion powershell | Out-String | Invoke-Expression - -To load completions for every new session, add the output of the above command -to your powershell profile. - -Usage: - supabase completion powershell [flags] - -Flags: - -h, --help help for powershell - --no-descriptions disable completion descriptions -``` - -#### completion zsh - -``` -Generate the autocompletion script for the zsh shell. - -If shell completion is not already enabled in your environment you will need -to enable it. You can execute the following once: - - echo "autoload -U compinit; compinit" >> ~/.zshrc - -To load completions in your current shell session: - - source <(supabase completion zsh) - -To load completions for every new session, execute once: - -#### Linux: - - supabase completion zsh > "${fpath[1]}/_supabase" - -#### macOS: - - supabase completion zsh > $(brew --prefix)/share/zsh/site-functions/_supabase - -You will need to start a new shell for this setup to take effect. - -Usage: - supabase completion zsh [flags] - -Flags: - -h, --help help for zsh - --no-descriptions disable completion descriptions -``` - -### help - -``` -Help provides help for any command in the application. -Simply type supabase help [path to command] for full details. - -Usage: - supabase help [command] [flags] - -Flags: - -h, --help help for help -``` diff --git a/apps/cli/docs/profile.md b/apps/cli/docs/profile.md index 59949999a6..b5b6065a67 100644 --- a/apps/cli/docs/profile.md +++ b/apps/cli/docs/profile.md @@ -4,8 +4,8 @@ This document captures the intended design for advanced non-default backend rout CLI. The file is intentionally named `profile.md` to preserve continuity with the earlier discussion -and the old Go CLI concept. The design in this document does **not** recommend carrying the old -`profile` abstraction forward. The preferred term in the new CLI is **platform**. +and the previous `profile` concept. The design in this document does **not** recommend carrying the +previous `profile` abstraction forward. The preferred term in the new CLI is **platform**. ## Overview @@ -21,9 +21,9 @@ A much smaller group of users needs to point the CLI somewhere else: The goal is to support those cases cleanly without turning non-default backend routing into a first-class everyday feature. -## Why Not Go-Style Profiles +## Why Not Profiles -The old Go CLI `profile` abstraction became too broad. +The previous `profile` abstraction became too broad. It started as a way to switch API endpoints, but it ended up bundling multiple unrelated concerns: @@ -195,7 +195,7 @@ This design does not propose: - a built-in first-party staging alias in the public UX - customer-specific platform definitions baked into the CLI - plugin or product behavior encoded directly inside platform config -- backward compatibility with Go CLI `profile` names or storage behavior +- backward compatibility with previous `profile` names or storage behavior ## Open Follow-up Work diff --git a/apps/cli/docs/release-process.md b/apps/cli/docs/release-process.md index de73842ecb..42784b6197 100644 --- a/apps/cli/docs/release-process.md +++ b/apps/cli/docs/release-process.md @@ -34,7 +34,6 @@ pnpm local-registry Publish the CLI into it from another terminal (current platform only, faster than a cross-platform build): ```sh -# CLI (Bun SFE + Go sidecar — requires Go on PATH and `pnpm repos:install`): pnpm cli-release ``` @@ -44,7 +43,7 @@ Test it: npx --registry http://localhost:4873 supabase@ --version ``` -`[tools/release/local-release.ts](../../../tools/release/local-release.ts)` does the heavy lifting: it builds the platform SFE (+ Go sidecar) and the umbrella `supabase` package, materialises them in a `tmp` dir (so no workspace `package.json` is modified), and publishes both to Verdaccio. The cleanup is automatic even on failure. +`[tools/release/local-release.ts](../../../tools/release/local-release.ts)` does the heavy lifting: it builds the platform SFE and the umbrella `supabase` package, materialises them in a `tmp` dir (so no workspace `package.json` is modified), and publishes both to Verdaccio. The cleanup is automatic even on failure. This is the right ring for: @@ -60,7 +59,7 @@ It is **not** a valid test for Homebrew or Scoop — those paths are covered in This is how you validate the Homebrew formula, Scoop manifest, and GitHub-Release-host resolution on real infrastructure without touching `supabase/`\* repos or risking a clash with an already-installed `supabase` CLI on the reviewer's machine. -Both updater scripts support a `--name ` flag that pushes the formula / manifest under a different name (e.g., `supabase-shim-poc`) — that is, a different filename and Ruby class / scoop manifest. The installed binary is always `supabase` (matching the Go CLI), so PoC reviewers should `brew uninstall supabase` / `scoop uninstall supabase` first if they already have the official CLI installed. +Both updater scripts support a `--name ` flag that pushes the formula / manifest under a different name (e.g., `supabase-shim-poc`) — that is, a different filename and Ruby class / scoop manifest. The installed binary is always `supabase`, so PoC reviewers should `brew uninstall supabase` / `scoop uninstall supabase` first if they already have the official CLI installed. ### One-time setup (per reviewer) @@ -87,7 +86,6 @@ The `--dry-run` flag on both updater scripts produces the `Formula/.rb` an ```sh # Build all eight platform archives + linux packages + checksums.txt. -# Ships the Go sidecar alongside the Bun SFE. bun apps/cli/scripts/build.ts --version 0.0.1 # Render the Homebrew formula against your PoC release host + tap. @@ -176,13 +174,13 @@ Validated on Windows x64 (`v0.0.1`, 2026-04-21): installed with no SmartScreen b ### What to validate -Beyond `--version` and `brew test`, exercise a Phase-0 proxied subcommand that requires the `supabase-go` sidecar: +Beyond `--version` and `brew test`, exercise the actual command tree rather than only a flag resolved at build time: ```sh -supabase completion bash +supabase --help ``` -This must spawn the colocated `supabase-go` and print the generated completion script — not return `NotFound: ChildProcess.spawn (supabase ...)`. (`supabase --version` is served by the Bun wrapper and never touches the sidecar, so it is not a sufficient check on its own.) If it fails, the Homebrew install step is wrong: check that `[apps/cli/scripts/update-homebrew.ts](../scripts/update-homebrew.ts)`'s install-lines block ran `bin.install "supabase-go" if File.exist?("supabase-go")`, and that the built archive actually contains `supabase-go` (it should, for any release build). +This must print the full command tree, not fail or truncate — a corrupted or partial binary can still resolve `--version` (a build-time `--define`) while failing on real command dispatch. ### Local-artifact testing (no GitHub Release upload) @@ -498,12 +496,12 @@ Keep two to three stable majors supported at the same time, roughly six to twelv `**build` (ubuntu-latest):\*\* 1. `[bun apps/cli/scripts/sync-versions.ts --version X.Y.Z](../scripts/sync-versions.ts)` — writes the release version into every `package.json` (umbrella + eight platform packages) and resolves the umbrella's `workspace:`\* `optionalDependencies` to `X.Y.Z`. -2. `[bun apps/cli/scripts/build.ts --version X.Y.Z](../scripts/build.ts)` — cross-compiles the Bun SFE for all eight targets (including windows-arm64), cross-compiles the Go sidecar, **ad-hoc signs the macOS binaries** (see [Code signing (macOS)](#code-signing-macos)), builds the six Linux packages via `nfpm`, produces the tar/zip archives, and writes `dist/checksums.txt`. +2. `[bun apps/cli/scripts/build.ts --version X.Y.Z](../scripts/build.ts)` — cross-compiles the Bun SFE for all eight targets (including windows-arm64), **ad-hoc signs the macOS binaries** (see [Code signing (macOS)](#code-signing-macos)), builds the six Linux packages via `nfpm`, produces the tar/zip archives, and writes `dist/checksums.txt`. 3. `actions/upload-artifact` preserves `packages/cli-*/bin/` and `dist/` for the downstream jobs. `**smoke-test` (matrix: `ubuntu-latest`, `macos-latest`, `macos-15-intel`, `windows-latest`):\*\* -Downloads the build artifact, makes the SFE executable (`chmod +x` on non-Windows), installs Scoop on Windows, and runs `pnpm run test:smoke -- --version X.Y.Z --tag .stable>` from `apps/cli`. On the macOS legs this also verifies each binary's signature (`codesign --verify --strict`, correct identifier, not linker-signed) and executes `supabase --version`, which is the real AMFI gate. Any failure blocks publishing. +Downloads the build artifact, makes the SFE executable (`chmod +x` on non-Windows), installs Scoop on Windows, and runs `pnpm run test:smoke -- --version X.Y.Z --tag .stable>` from `apps/cli`. On the macOS legs this also verifies the binary's signature (`codesign --verify --strict`, correct identifier, not linker-signed) and executes `supabase --version`, which is the real AMFI gate. Any failure blocks publishing. The matrix does not yet include `windows-11-arm` (gate 6) or an Alpine musl runner (also gate 6). Until those land, arm64 / musl regressions only surface in Ring 2 validation. @@ -532,13 +530,13 @@ Both updaters run automatically from `release-shared.yml`'s `publish-homebrew` a Once the channels are live, two reusable workflows run automatically (last in `release-shared.yml`, non-gating — by the time they run the artifacts are already published, so a failure surfaces as a red post-release signal rather than blocking distribution): - `[setup-cli-smoke-test.yml](../../../.github/workflows/setup-cli-smoke-test.yml)` (`setup-cli-smoke` job) — installs the released version through `supabase/setup-cli` (the GitHub Release download path) on Linux, macOS, Windows, and Alpine. -- `[verify-install-channels.yml](../../../.github/workflows/verify-install-channels.yml)` (`verify-install-channels` job) — runs a **real** `brew install` (macOS **and** Linux, so both the `on_macos` and `on_linux` stanzas of the formula are exercised), `scoop install`, and `curl|bash` install of the **published** install script (fetched from the release asset, not the repo checkout) against the just-published Homebrew tap, Scoop bucket, and GitHub Release. Each leg then asserts `supabase --version` matches and runs `supabase completion bash` (a Go-proxied command) so a package that omits or misplaces the `supabase-go` sidecar fails too. brew, scoop, and the install script each verify the published `sha256`/`hash` against the downloaded tarball, so this is the signal that would have caught CLI v2.107.0 (where the brew/scoop manifests shipped checksums that did not match the release tarballs and every `brew install` / `scoop install` failed). It only runs for `beta`/`stable`/`maintenance` (the channels that publish brew/scoop) and can be dispatched manually against any already-published version via the Actions tab. +- `[verify-install-channels.yml](../../../.github/workflows/verify-install-channels.yml)` (`verify-install-channels` job) — runs a **real** `brew install` (macOS **and** Linux, so both the `on_macos` and `on_linux` stanzas of the formula are exercised), `scoop install`, and `curl|bash` install of the **published** install script (fetched from the release asset, not the repo checkout) against the just-published Homebrew tap, Scoop bucket, and GitHub Release. Each leg then asserts `supabase --version` matches and runs `supabase --help` to exercise the actual command tree. brew, scoop, and the install script each verify the published `sha256`/`hash` against the downloaded tarball, so this is the signal that would have caught CLI v2.107.0 (where the brew/scoop manifests shipped checksums that did not match the release tarballs and every `brew install` / `scoop install` failed). It only runs for `beta`/`stable`/`maintenance` (the channels that publish brew/scoop) and can be dispatched manually against any already-published version via the Actions tab. ### Code signing (macOS) -The macOS binaries (`supabase` Bun SFE + `supabase-go` sidecar, `darwin-arm64` and `darwin-x64`) are signed inside `build.ts` between compilation and archiving, so the signed bytes flow into every channel that consumes `packages/cli-darwin-*/bin/` — npm platform packages, Homebrew, and the GitHub Release tarballs (which also feed the `install` script and `setup-cli`). Background: [ADR 0014](../../../docs/adr/0014-macos-code-signing-and-notarization.md). +The macOS binary (`supabase` Bun SFE, `darwin-arm64` and `darwin-x64`) is signed inside `build.ts` between compilation and archiving, so the signed bytes flow into every channel that consumes `packages/cli-darwin-*/bin/` — npm platform packages, Homebrew, and the GitHub Release tarballs (which also feed the `install` script and `setup-cli`). Background: [ADR 0014](../../../docs/adr/0014-macos-code-signing-and-notarization.md). -Why this exists: `bun build --compile` and the Go linker emit only a degenerate "linker-signed" ad-hoc signature (identifier `a.out`, no requirements blob). macOS 26+ AMFI rejects it and SIGKILLs the process at launch ([CLI-1621](https://linear.app/supabase/issue/CLI-1621) / [#5556](https://github.com/supabase/cli/issues/5556)). A full ad-hoc signature fixes it. +Why this exists: `bun build --compile` emits only a degenerate "linker-signed" ad-hoc signature (identifier `a.out`, no requirements blob). macOS 26+ AMFI rejects it and SIGKILLs the process at launch ([CLI-1621](https://linear.app/supabase/issue/CLI-1621) / [#5556](https://github.com/supabase/cli/issues/5556)). A full ad-hoc signature fixes it. - **Signing runs on the Linux build runner** via [`rcodesign`](https://github.com/indygreg/apple-platform-rs) (the apple-codesign project), which signs Mach-O binaries without a macOS host. No macOS signing job exists, and **no Apple credentials are required for the current ad-hoc signing** (Phase 1). The version + sha256 are pinned in the "Install rcodesign" step of [`build-cli-artifacts.yml`](../../../.github/workflows/build-cli-artifacts.yml). - **CI hard-fails if signing is unavailable**: the build job sets `SUPABASE_CLI_REQUIRE_SIGNING=1`, so a missing `rcodesign` fails the build rather than silently shipping unsigned binaries. Local builds without `rcodesign` degrade to a warning and skip signing. @@ -585,7 +583,7 @@ There is no rollback. Published artifacts are immutable, and repointing npm `lat - [ADR 0011](../../../docs/adr/0011-cli-release-and-distribution-strategy.md) — the decision record. Channel choices, signing rationale, open pre-cutover gates. - [ADR 0028](../../../docs/adr/0028-release-branches-and-maintenance-lines.md) — the `next` channel and `v.x` maintenance lines. - [ADR 0030](../../../docs/adr/0030-hotfix-releases.md) — hotfix criteria, forward-only releases, and the required `Check deploy`. -- `[apps/cli/docs/binary-distribution.md](./binary-distribution.md)` — why each platform package contains two binaries (`supabase` SFE + `supabase-go` sidecar) and how they're resolved at runtime. +- `[apps/cli/docs/binary-distribution.md](./binary-distribution.md)` — how each platform package's `supabase` binary is built and resolved at runtime. - `[tools/release/local-release.ts](../../../tools/release/local-release.ts)` — Ring 1 implementation. - `[apps/cli/scripts/build.ts](../scripts/build.ts)`, `[publish.ts](../scripts/publish.ts)`, `[sync-versions.ts](../scripts/sync-versions.ts)`, `[update-homebrew.ts](../scripts/update-homebrew.ts)`, `[update-scoop.ts](../scripts/update-scoop.ts)` — release script implementations. - `[.github/workflows/release.yml](../../../.github/workflows/release.yml)`, `[release-shared.yml](../../../.github/workflows/release-shared.yml)`, `[deploy.yml](../../../.github/workflows/deploy.yml)`, `[deploy-check.yml](../../../.github/workflows/deploy-check.yml)`, `[fast-forward.yml](../../../.github/workflows/fast-forward.yml)`, `[sync-branches.yml](../../../.github/workflows/sync-branches.yml)`, `[branch-policy.yml](../../../.github/workflows/branch-policy.yml)` — Ring 3 pipeline and branch promotion. diff --git a/apps/cli/docs/stack-commands.md b/apps/cli/docs/stack-commands.md index 8c8721c4bb..e26d1f5ed9 100644 --- a/apps/cli/docs/stack-commands.md +++ b/apps/cli/docs/stack-commands.md @@ -139,10 +139,9 @@ are quoted so that sourcing the file performs no shell expansion. The exported v `PUBLISHABLE_KEY`, `SECRET_KEY`, `ANON_KEY`, and `SERVICE_ROLE_KEY`; `REST_URL` and `FUNCTIONS_URL` are `/rest/v1` and `/functions/v1`, `MCP_URL` is `/mcp`, whose port is assigned per project, and `DB_URL` uses the `postgres` role with the saved database -password, URI-encoded, and no query string. `INBUCKET_URL` is also exported alongside -`MAILPIT_URL`, with the same value, as a deprecated alias. `start` and `status` text output shows -only the publishable and secret keys; `ANON_KEY` and `SERVICE_ROLE_KEY` appear only in JSON `env` -and `status --env`. `start` and `status` JSON/stream-JSON results include this same connection map +password, URI-encoded, and no query string. `start` and `status` text output shows only the +publishable and secret keys; `ANON_KEY` and `SERVICE_ROLE_KEY` appear only in JSON `env` and +`status --env`. `start` and `status` JSON/stream-JSON results include this same connection map under `env`; `status --env` remains the dotenv/variable-map export, and `--override-name` only applies there. `--override-name` accepts repeated or comma-separated `EXPORTED_VARIABLE=NAME` entries, requires `--env`, and rejects unknown variables, invalid names, and collisions. The @@ -190,9 +189,9 @@ unavailable. An invalid `SUPABASE_EXPERIMENTAL_STACK` value is still an error. For temporary selection, set `SUPABASE_EXPERIMENTAL_STACK=1` to select the new backend or `SUPABASE_EXPERIMENTAL_STACK=0` to select the legacy backend. This environment variable takes precedence over `experimental.stack`; an unset or empty value falls back to the file setting. -Other values are rejected, including by `supabase link`, `supabase unlink` and `supabase projects -delete`, which read or write the project link. The override is applied before reading the project -configuration. +Non-boolean values are rejected, including by `supabase link`, `supabase unlink` and `supabase +projects delete`, which read or write the project link. The override is applied before reading the +project configuration. `supabase services` follows the same backend selection. In stack mode it lists image versions and canonical `ghcr.io/supabase/cli/...` names, including Mailpit. The versions come from @@ -212,16 +211,16 @@ project config and omits the Docker-era default ports so the stack is not pinned Without the environment variable, `init` still writes the established template with those ports and without the stack flag. Blank `supabase bootstrap` uses the same scaffold. -When the flag is on, `--local` targets of the `db`, `migration`, `test db`, `gen types`, and +When the flag is on, local targets of the `db`, `migration`, `test db`, `gen types`, and `inspect` families use the project stack and provision a throwaway shadow database owned by the stack runtime. The shadow runs the project's resolved database version, the OrioleDB build when `db.orioledb_version` is set, and its baseline cache is keyed by that version. Top-level -`supabase pull` uses the same stack shadow as `db pull`. Linked and `--db-url` targets stay on the Management API for engine selection. -A `--db-url` that matches `config.toml` host and port is still rewritten like a published +`supabase pull` uses the same stack shadow as `db pull`. Targets selected with `--remote` stay on the Management API for engine selection. +A `--remote=` database URL that matches `config.toml` host and port is still rewritten like a published stack target for dump's tool container. Compose names (`supabase_db_*`, `supabase_network_*`, `db:5432`) are not used. The stack backend requires the in-process pg-delta engine; `--use-migra`, `--use-pgadmin`, `--use-pg-schema`, and `db pull --diff-engine migra` are -rejected. The flag also routes local `functions serve` through the stack; other `functions` +rejected (`--use-migra=false` and `--use-pgadmin=false` keep pg-delta and are accepted). The flag also routes local `functions serve` through the stack; other `functions` commands retain their existing behavior. `functions serve` requires a running project stack. It attaches to the existing Functions member, @@ -244,34 +243,33 @@ and restores default JWT verification on the same Functions instance. Per-functi `--output-format stream-json`, readiness emits a result containing the instance ID and URL, followed by live log events. -`storage ls`/`cp`/`mv`/`rm` and `seed buckets` (including bucket seeding inside `db reset ---local`) also consult `experimental.stack`, with the same `SUPABASE_EXPERIMENTAL_STACK` +`storage ls`/`cp`/`mv`/`rm` and `seed buckets` (including bucket seeding inside `db reset`) also consult `experimental.stack`, with the same `SUPABASE_EXPERIMENTAL_STACK` env-precedence rule as `start`/`stop`/`status`. See -[Storage and bucket seeding](#storage-and-bucket-seeding) below. Explicit `--linked`/ -`--project-ref` remote targeting for these commands is unaffected by the flag either way. +[Storage and bucket seeding](#storage-and-bucket-seeding) below. Explicit `--remote` +targeting for these commands is unaffected by the flag either way. `db start` brings up a postgres-only project stack on first create. An existing stack resumes its primary database without changing other services (webhooks setup only, creating a missing `supabase_functions` schema; no second overlay or migrate-and-seed). `supabase start` while that postgres-only stack is running stops it and starts the full configured stack, keeping data. `--from-backup` is not supported on the stack path. -`db reset --local` and declarative resets rebuild the existing database while retaining stack +A local `db reset` and declarative resets rebuild the existing database while retaining stack identity, ports, and composition. See [`db/reset/SIDE_EFFECTS.md`](../src/commands/db/reset/SIDE_EFFECTS.md). -`gen types --local` and `inspect db … --local` resolve the project stack through the same -`--local` database target as `db dump`. They do not start a stack. +A local `gen types` and `inspect db …` resolve the project stack through the same +local database target as `db dump`. They do not start a stack. -On the stack backend, `db dump --local`, `migration squash`, and `test db --local` run +On the stack backend, a local `db dump`, `migration squash`, and a local `test db` run catalog tools attached to their owning stack. Tools use runtime-facing database addresses; `test db` preserves the user, password, and database selected by the CLI resolver. -External `--linked` and `--db-url` tools use CLI-owned adapters without creating a stack. +External `--remote` tools use CLI-owned adapters without creating a stack. Supported native platforms run the bundled executable with `artifact/bin` on PATH. Other platforms use a one-shot container from the catalog Postgres image. Neither path falls back to host-installed PostgreSQL tools. Missing tools fail closed; container execution requires Docker. -`db lint --local` talks to the running stack over SQL. Catalog Postgres includes +A local `db lint` talks to the running stack over SQL. Catalog Postgres includes `plpgsql_check`, so native and container stacks can `CREATE EXTENSION` inside the lint transaction (always rolled back). It does not launch a client binary. @@ -411,12 +409,12 @@ to be destroyed does not stop the rest, though the command still fails. The `experimental.stack` flag (with the usual `SUPABASE_EXPERIMENTAL_STACK=1|0` env precedence) routes several command families at once: the top-level `start`/`stop`/`status` -aliases, the `stack` namespace itself, `db`/`migration`/`test`'s `--local` targets (above), and +aliases, the `stack` namespace itself, `db`/`migration`/`test`'s local targets (above), and now `storage ls`/`cp`/`mv`/`rm` and `seed buckets` (including the bucket seeding step inside `db -reset --local`). An explicit `--linked`/`--project-ref` remote target for `storage`/`seed` is +reset`). An explicit `--remote` target for `storage`/`seed` is unaffected by the flag either way. -Under the stack backend, `--local` Storage operations resolve their endpoint and credential from +Under the stack backend, local Storage operations resolve their endpoint and credential from the selected composition rather than `[api]`/`[api.tls]`: the endpoint is the Storage member's observed HTTP binding plus `/storage/v1/...`, and the service-role JWT is generated from the primary database member's observed JWT secret. The stack is located by the project root @@ -444,7 +442,7 @@ status`/`supabase stack restart`, or `supabase start` when the stack has never b Missing Storage HTTP endpoints fail with `StackStorageCapabilityError`, as do other Storage capability failures. A stack-gateway 502/503 encountered while Storage activates is reported as `StackStorageCapabilityError` guiding the user to `supabase stack logs` then `supabase stack -restart`, not as a raw status body — but only for a `--local` target; a `--linked` failure passes +restart`, not as a raw status body — but only for a local target; a `--remote` failure passes through unchanged. All of these failures exit `1` for `storage`/`seed buckets`, and no HTTP request is sent when Storage is disabled or the stack is not running. @@ -466,6 +464,6 @@ a missing capability/endpoint, a gateway activation failure, or an invalid bucke `start` command; startup does not silently skip the configured seed. See [`stack/start/SIDE_EFFECTS.md`](../src/commands/experimental/stack/start/SIDE_EFFECTS.md). -**`db reset --local` and declarative reset** reapply the catalog, migrations, and seeds before +**A local `db reset` and declarative reset** reapply the catalog, migrations, and seeds before resuming the saved composition. Bucket-seeding failures warn after the database reset succeeds. See [`db/reset/SIDE_EFFECTS.md`](../src/commands/db/reset/SIDE_EFFECTS.md). diff --git a/apps/cli/docs/supabase-home.md b/apps/cli/docs/supabase-home.md index 6d4a6b9e46..57e51097b8 100644 --- a/apps/cli/docs/supabase-home.md +++ b/apps/cli/docs/supabase-home.md @@ -25,7 +25,7 @@ User-authored configuration and migrations live in the repository: functions/ ``` -The CLI discovers the active project from the nearest ancestor containing `supabase/config.toml` +The CLI discovers the active project from the nearest ancestor containing `supabase/config.toml` or `supabase/config.json` (or the legacy config filename where supported), then passes that canonical project root to the managed stack identity. A stack ID is derived from the canonical project root, the current Git branch context (or `ordinary-workspace` outside Git), and the stack name. This identity is local diff --git a/apps/cli/docs/supabase/config/diff.md b/apps/cli/docs/supabase/config/diff.md index 229783224c..6f299df3ef 100644 --- a/apps/cli/docs/supabase/config/diff.md +++ b/apps/cli/docs/supabase/config/diff.md @@ -2,7 +2,7 @@ Shows the configuration differences between the local `supabase/config.toml` and the effective configuration of a remote project or branch. Read-only: it never modifies the local file or any remote configuration. -Pass `--project-ref` to compare against a specific project, or the name (or UUID) of a branch of the currently linked project — values that are exactly 20 lowercase letters are always treated as project refs. Without it, the linked project is the target. When the target ref matches a `[remotes.*]` block's `project_id`, that block's merged config is the local side of the comparison. +Pass `--remote=` to compare against a specific project, or the name (or UUID) of a branch of the currently linked project — values that are exactly 20 lowercase letters are always treated as project refs. Without it, the linked project is the target. `--project-ref` still works in v3 but is deprecated and removed in v4; use `--remote`. When the target ref matches a `[remotes.*]` block's `project_id`, that block's merged config is the local side of the comparison. Only platform-managed properties are compared. Local-stack-only sections — `[studio]`, `[analytics]`, `[functions]`, `[edge_runtime]`, port numbers, image version pins, `[db.migrations]`, and similar — have no hosted counterpart and are never reported, whether or not your file declares them. @@ -12,4 +12,4 @@ Local values are shown as the configuration your file would produce once pushed, With `--exit-code`, the command exits `2` when any difference is found, keeping exit `1` for errors — so scripts can distinguish drift from failure. -Machine-readable output is available through `--output-format json|stream-json` — a versioned payload (`schema_version`, `config_schema`, `target`, `scope`, `changes[]`, `masked[]`, `unmanaged[]`, `counts`) with per-change `path`s as segment arrays. The legacy global `-o`/`--output` flag is not supported by this command; use `--output-format json|stream-json` instead. +Machine-readable output is available through `--output-format json|stream-json` — a versioned payload (`schema_version`, `config_schema`, `target`, `scope`, `changes[]`, `masked[]`, `unmanaged[]`, `counts`) with per-change `path`s as segment arrays. `target` identifies the compared project: `kind` (`linked`), `project_ref`, `project_name` and `environment` (`production`, `branch` or `unverified`) when known, `parent_project_ref` for a branch, `branch` when it was named, and `local_scope` (`base` or `remotes.`). The legacy global `-o`/`--output` flag is not supported by this command; use `--output-format json|stream-json` instead. diff --git a/apps/cli/docs/supabase/config/pull.md b/apps/cli/docs/supabase/config/pull.md index 3e238c1d59..0b7878c0b5 100644 --- a/apps/cli/docs/supabase/config/pull.md +++ b/apps/cli/docs/supabase/config/pull.md @@ -2,9 +2,9 @@ Writes the effective configuration of a remote project or branch into the local `supabase/config.toml`/`config.json` — the write side of `supabase config diff`. Every write is a surgical, format-preserving edit: comments, key ordering, and quoting elsewhere in the file are left untouched, and only the values that actually change are rewritten. -Pass `--project-ref` to pull from a specific project, or the name (or UUID) of a branch of the currently linked project — values that are exactly 20 lowercase letters are always treated as project refs. Without it, the linked project is the target. +Pass `--remote=` to pull from a specific project, or the name (or UUID) of a branch of the currently linked project — values that are exactly 20 lowercase letters are always treated as project refs. Without it, the linked project is the target. `--project-ref` still works in v3 but is deprecated and removed in v4; use `--remote`. -Where a pulled value lands depends on whether the target is already tracked by a `[remotes.*]` block, not on how you named it: if any block's `project_id` already matches the resolved ref, that block is reused — whatever its own label — and every value lands there. Otherwise, if the target was named as a branch, a new `[remotes.]` block is created (a branch resolved by UUID falls back to the project ref itself as its label). Only when neither applies — a bare `--project-ref` naming a project directly, or the linked project with no branch involved — do values land at the config root. `--remote-label` overrides the block config pull would otherwise reuse or create; naming a block that already tracks a different project, or naming nothing while a different block already tracks this exact ref, is an error rather than a silent overwrite. A `[remotes.*]` block whose `project_id` is an `env(...)` reference that merely _resolves_ to the target ref is never reused or rewritten — that is a hard error naming the variable, since a value written there would never actually take effect on the next load. Creating a new block always writes its `project_id`, even when every value it would otherwise carry already matches the local defaults — otherwise the block could never be reused on a later pull. +Where a pulled value lands depends on whether the target is already tracked by a `[remotes.*]` block, not on how you named it: if any block's `project_id` already matches the resolved ref, that block is reused — whatever its own label — and every value lands there. Otherwise, if the target was named as a branch, a new `[remotes.]` block is created (a branch resolved by UUID falls back to the project ref itself as its label). Only when neither applies — `--remote=` naming a project directly, or the linked project (a bare `--remote` or no flag) with no branch involved — do values land at the config root. `--remote-label` overrides the block config pull would otherwise reuse or create; naming a block that already tracks a different project, or naming nothing while a different block already tracks this exact ref, is an error rather than a silent overwrite. A `[remotes.*]` block whose `project_id` is an `env(...)` reference that merely _resolves_ to the target ref is never reused or rewritten — that is a hard error naming the variable, since a value written there would never actually take effect on the next load. Creating a new block always writes its `project_id`, even when every value it would otherwise carry already matches the local defaults — otherwise the block could never be reused on a later pull. Writing to the config root can also affect `supabase start`: a handful of root-scoped settings — `auth.site_url`, `db.settings.*`, `db.major_version` (which changes what `supabase start` boots), `db.pooler.*`, and similar — also govern the local stack, so pulling a hosted value into them is a real change to local dev behavior, not just a record of the hosted project's own setting. `config pull` warns about this rather than refusing; writing the same value into a `[remotes.*]` block is unaffected. Passing `--remote-label` is the escape hatch: it diverts what would otherwise be a root-bound pull into a `[remotes.

hi

" })), + ); it.effect("rejects an email template whose content_path file is missing", () => failsWith( ["[auth.email.template.invite]", 'content_path = "./missing.html"'], @@ -2509,11 +2458,31 @@ describe("readDbToml auth.Enabled validation (Go config.Validate parity)", () => "Missing required field in config: auth.external.github.client_id", ), ); + it.effect("rejects an enabled custom external provider without a client_id", () => + failsWith( + ["[auth.external.my_oidc]", "enabled = true"], + "Missing required field in config: auth.external.my_oidc.client_id", + ), + ); + it.effect("rejects an enabled custom external provider without a secret", () => + failsWith( + ["[auth.external.my_oidc]", "enabled = true", 'client_id = "cid"'], + "Missing required field in config: auth.external.my_oidc.secret", + ), + ); + it.effect("accepts a custom external provider whose secret comes from the shell", () => + succeeds(["[auth.external.my_oidc]", "enabled = true", 'client_id = "cid"']).pipe( + withEnv({ SUPABASE_AUTH_EXTERNAL_MY_OIDC_SECRET: "shh" }), + ), + ); it.effect("exempts apple/google from the external secret requirement", () => succeeds(["[auth.external.apple]", "enabled = true", 'client_id = "a"']), ); - it.effect("never validates the deprecated linkedin/slack providers", () => - succeeds(["[auth.external.linkedin]", "enabled = true"]), + it.effect("rejects the removed linkedin provider, naming linkedin_oidc", () => + failsWith( + ["[auth.external.linkedin]", "enabled = true"], + "[auth.external.linkedin] in supabase/config.toml is no longer supported. Use [auth.external.linkedin_oidc] instead.", + ), ); it.effect("rejects an enabled firebase third_party without project_id", () => @@ -2561,7 +2530,7 @@ describe("readDbToml auth.Enabled validation (Go config.Validate parity)", () => ), ); - it.effect("defaults [auth.email.smtp] enabled=true when the table omits enabled (Go merge)", () => + it.effect("defaults [auth.email.smtp] enabled=true when the table omits enabled", () => failsWith( ["[auth.email.smtp]", 'user = "u"'], "Missing required field in config: auth.email.smtp.host", @@ -2584,19 +2553,19 @@ describe("readDbToml auth.Enabled validation (Go config.Validate parity)", () => it.effect("fails on a malformed auth boolean string instead of coercing to false", () => failsWith( ["[auth.passkey]", 'enabled = "maybe"'], - "failed to parse config: invalid auth.passkey.enabled.", + "Invalid auth.passkey.enabled in supabase/config.toml", ), ); - it.effect("rejects an unknown captcha provider (Go enum, regardless of enabled)", () => + it.effect("rejects an unknown captcha provider, regardless of enabled", () => failsWith( ["[auth.captcha]", "enabled = false", 'provider = "cloudflare"'], - "'auth.captcha.provider' must be one of [hcaptcha turnstile]", + "Invalid auth.captcha.provider in supabase/config.toml", ), ); }); -describe("readDbToml encrypted secret decryption (Go DecryptSecretHookFunc parity)", () => { +describe("readDbToml encrypted secret decryption", () => { // An undecryptable `encrypted:` value anywhere in config.toml aborts the load with // `failed to parse config: `. const expectFails = (lines: ReadonlyArray, message: string) => @@ -2633,7 +2602,7 @@ describe("readDbToml encrypted secret decryption (Go DecryptSecretHookFunc parit it.effect("accepts a plain (non-encrypted) secret value", () => expectLoads(["[db]", 'root_key = "plaintext-not-encrypted"']), ); - it.effect("treats an unset env() secret as a no-op (verbatim, like Go's hook)", () => + it.effect("treats an unset env() secret as a no-op (verbatim)", () => expectLoads(["[db]", 'root_key = "env(SOME_UNSET_ROOT_KEY)"']), ); it.effect("does NOT decrypt a non-secret string that starts with encrypted:", () => @@ -2678,7 +2647,7 @@ describe("readDbToml encrypted secret decryption (Go DecryptSecretHookFunc parit ); }); -describe("readDbToml non-scalar config booleans (Go UnmarshalExact parity)", () => { +describe("readDbToml non-scalar config booleans", () => { // A present non-scalar boolean must fail the config load rather than falling through to the // schema default, which would let `db reset` prompt and drop schemas. const failsInvalid = (lines: ReadonlyArray, field: string) => @@ -2687,7 +2656,7 @@ describe("readDbToml non-scalar config booleans (Go UnmarshalExact parity)", () const exit = yield* read(dir).pipe(Effect.exit); expect(Exit.isFailure(exit)).toBe(true); if (Exit.isFailure(exit)) { - expect(Cause.pretty(exit.cause)).toContain(`failed to parse config: invalid ${field}.`); + expect(Cause.pretty(exit.cause)).toContain(`Invalid ${field} in supabase/config.toml`); } }); it.effect("rejects an array value for [db.migrations] enabled", () => @@ -2698,7 +2667,61 @@ describe("readDbToml non-scalar config booleans (Go UnmarshalExact parity)", () ); }); -describe("readDbToml empty project_id (Go config.Validate parity)", () => { +describe("readDbToml project env scoping", () => { + const LINKED = "linkedlinkedlinkedlin"; + const OTHER = "otherotherotherother"; + + const withEnvFile = (config: string, dotEnv: string, linkedRef?: string) => + withConfig(config).pipe( + Effect.tap((dir) => writeFile(dir, ["supabase", ".env"], dotEnv)), + Effect.tap((dir) => + linkedRef === undefined + ? Effect.void + : makeDir(dir, ["supabase", ".temp"]).pipe( + Effect.andThen(writeFile(dir, ["supabase", ".temp", "project-ref"], linkedRef)), + ), + ), + ); + + it.effect("omits a linked project's .env credential when targeting another project", () => { + return withEnvFile( + 'project_id = "demo"\n', + "SUPABASE_DB_PASSWORD=linked-secret\nUNRELATED=kept\n", + LINKED, + ).pipe( + Effect.flatMap((dir) => readRef(dir, OTHER)), + Effect.tap((v) => + Effect.sync(() => { + expect(v.projectEnv).toEqual({ UNRELATED: "kept" }); + }), + ), + ); + }); + + it.effect("keeps the .env credential when targeting the linked project", () => { + return withEnvFile( + 'project_id = "demo"\n', + "SUPABASE_DB_PASSWORD=linked-secret\n", + LINKED, + ).pipe( + Effect.flatMap((dir) => readRef(dir, LINKED)), + Effect.tap((v) => + Effect.sync(() => { + expect(v.projectEnv).toEqual({ SUPABASE_DB_PASSWORD: "linked-secret" }); + }), + ), + ); + }); + + it.effect("lets an empty shell value shadow the .env value an env() secret refers to", () => { + return withEnvFile( + '[db]\nroot_key = "env(SHADOWED_ROOT_KEY)"\n', + "SHADOWED_ROOT_KEY=encrypted:undecryptable\n", + ).pipe(Effect.flatMap(read), withEnv({ SHADOWED_ROOT_KEY: "" })); + }); +}); + +describe("readDbToml empty project_id", () => { it.effect("rejects a present-but-empty top-level project_id", () => { return withConfig('project_id = ""\n').pipe( Effect.flatMap(read), @@ -2728,7 +2751,7 @@ describe("readDbToml empty project_id (Go config.Validate parity)", () => { }); }); -describe("readDbToml [analytics] validation (Go config.Validate parity)", () => { +describe("readDbToml [analytics] validation", () => { const failsWith = (lines: ReadonlyArray, message: string) => Effect.gen(function* () { const dir = yield* withConfig(lines.join("\n")); @@ -2746,7 +2769,7 @@ describe("readDbToml [analytics] validation (Go config.Validate parity)", () => it.effect("rejects an unknown analytics.backend regardless of enabled", () => failsWith( ["[analytics]", "enabled = false", 'backend = "clickhouse"'], - "'analytics.backend' must be one of [postgres bigquery]", + "Invalid analytics.backend in supabase/config.toml", ), ); it.effect("rejects bigquery analytics missing gcp_project_id", () => @@ -2800,13 +2823,13 @@ describe("readDbToml [analytics] validation (Go config.Validate parity)", () => }); }); -describe("readDbToml SUPABASE_PROJECT_ID override (Go AutomaticEnv parity)", () => { +describe("readDbToml SUPABASE_PROJECT_ID override", () => { it.effect("overrides the TOML project_id with SUPABASE_PROJECT_ID", () => { return withConfig(['project_id = "toml-project"', ""].join("\n")).pipe( Effect.flatMap(read), Effect.tap((v) => Effect.sync(() => { - expect(Option.getOrNull(v.projectId)).toBe("env-project"); + expect(v.projectId).toBe("env-project"); }), ), withEnv({ SUPABASE_PROJECT_ID: "env-project" }), @@ -2818,147 +2841,54 @@ describe("readDbToml SUPABASE_PROJECT_ID override (Go AutomaticEnv parity)", () Effect.flatMap(read), Effect.tap((v) => Effect.sync(() => { - expect(Option.getOrNull(v.projectId)).toBe("env-project"); + expect(v.projectId).toBe("env-project"); }), ), withEnv({ SUPABASE_PROJECT_ID: "env-project" }), ); }); - it.effect("ignores an empty SUPABASE_PROJECT_ID (viper AllowEmptyEnv=false)", () => { + it.effect("ignores an empty SUPABASE_PROJECT_ID", () => { return withConfig(['project_id = "toml-project"', ""].join("\n")).pipe( Effect.flatMap(read), Effect.tap((v) => Effect.sync(() => { - expect(Option.getOrNull(v.projectId)).toBe("toml-project"); + expect(v.projectId).toBe("toml-project"); }), ), withEnv({ SUPABASE_PROJECT_ID: "" }), ); }); - it.effect( - "prefers a matched [remotes.]'s project_id over a conflicting SUPABASE_PROJECT_ID", - () => { - const ref = "abcdefghijklmnopqrst"; - return withConfig( - ['project_id = "toml-project"', "[remotes.prod]", `project_id = "${ref}"`, ""].join("\n"), - ).pipe( - Effect.flatMap((dir) => readRef(dir, ref)), - Effect.tap((v) => - Effect.sync(() => { - expect(v.appliedRemote).toBe("prod"); - expect(v.remoteOverrideKeys.has("project_id")).toBe(true); - expect(Option.getOrNull(v.projectId)).toBe(ref); - }), - ), - withEnv({ SUPABASE_PROJECT_ID: "local" }), - ); - }, - ); - - it.effect("still applies SUPABASE_PROJECT_ID when no [remotes.*] block matches the ref", () => { + it.effect("SUPABASE_PROJECT_ID beats a matched [remotes.]'s project_id", () => { const ref = "abcdefghijklmnopqrst"; - return withConfig(['project_id = "toml-project"', ""].join("\n")).pipe( - Effect.flatMap((dir) => readRef(dir, ref)), - Effect.tap((v) => - Effect.sync(() => { - expect(v.appliedRemote).toBeUndefined(); - expect(Option.getOrNull(v.projectId)).toBe("env-project"); - }), - ), - withEnv({ SUPABASE_PROJECT_ID: "env-project" }), - ); - }); -}); - -describe("readDbToml remoteOverrideKeys — auth.captcha.provider / auth.email.template/notification", () => { - const ref = "abcdefghijklmnopqrst"; - - it.effect("tracks auth.captcha.provider when a matched remote block supplies it", () => { - // `provider` is a plain string leaf, not part of a dynamically-keyed section, so it must be - // tracked via `ENV_OVERRIDABLE_KEYS` like any other fixed-name field. return withConfig( - [ - "[auth.captcha]", - 'provider = "hcaptcha"', - "[remotes.prod]", - `project_id = "${ref}"`, - "[remotes.prod.auth.captcha]", - 'provider = "turnstile"', - "", - ].join("\n"), + ['project_id = "toml-project"', "[remotes.prod]", `project_id = "${ref}"`, ""].join("\n"), ).pipe( Effect.flatMap((dir) => readRef(dir, ref)), Effect.tap((v) => Effect.sync(() => { expect(v.appliedRemote).toBe("prod"); - expect(v.remoteOverrideKeys.has("auth.captcha.provider")).toBe(true); + expect(v.projectId).toBe("local"); }), ), + withEnv({ SUPABASE_PROJECT_ID: "local" }), ); }); - it.effect("tracks a matched remote block's auth.email.template. leaves dynamically", () => { - // `auth.email.template..*` is an arbitrarily-keyed map, same shape as - // `auth.external..*`, so it must be flattened dynamically instead of relying on a - // fixed `ENV_OVERRIDABLE_KEYS` entry. - return withConfig( - [ - "[remotes.prod]", - `project_id = "${ref}"`, - "[remotes.prod.auth.email.template.invite]", - 'content_path = "remote-invite.html"', - "", - ].join("\n"), - ).pipe( - // Template `content_path` resolves relative to the project root (`workdir`, i.e. `dir`). - Effect.tap((dir) => writeFile(dir, ["remote-invite.html"], "")), + it.effect("still applies SUPABASE_PROJECT_ID when no [remotes.*] block matches the ref", () => { + const ref = "abcdefghijklmnopqrst"; + return withConfig(['project_id = "toml-project"', ""].join("\n")).pipe( Effect.flatMap((dir) => readRef(dir, ref)), Effect.tap((v) => Effect.sync(() => { - expect(v.appliedRemote).toBe("prod"); - expect(v.remoteOverrideKeys.has("auth.email.template.invite.content_path")).toBe(true); - expect(v.remoteOverrideKeys.has("auth.email.template.invite.subject")).toBe(false); + expect(v.appliedRemote).toBeUndefined(); + expect(v.projectId).toBe("env-project"); }), ), + withEnv({ SUPABASE_PROJECT_ID: "env-project" }), ); }); - - it.effect( - "tracks a matched remote block's auth.email.notification. leaves dynamically", - () => { - // Sibling case to auth.email.template, including a boolean leaf (`enabled`). - return withConfig( - [ - "[remotes.prod]", - `project_id = "${ref}"`, - "[remotes.prod.auth.email.notification.password_changed]", - "enabled = true", - 'content_path = "remote-pw-changed.html"', - "", - ].join("\n"), - ).pipe( - // Notification `content_path` resolves relative to the project root, like a template. - Effect.tap((dir) => writeFile(dir, ["remote-pw-changed.html"], "")), - Effect.flatMap((dir) => readRef(dir, ref)), - Effect.tap((v) => - Effect.sync(() => { - expect(v.appliedRemote).toBe("prod"); - expect( - v.remoteOverrideKeys.has("auth.email.notification.password_changed.enabled"), - ).toBe(true); - expect( - v.remoteOverrideKeys.has("auth.email.notification.password_changed.content_path"), - ).toBe(true); - expect( - v.remoteOverrideKeys.has("auth.email.notification.password_changed.subject"), - ).toBe(false); - }), - ), - ); - }, - ); }); describe("readDbToml OrioleDB telemetry", () => { @@ -3018,34 +2948,4 @@ describe("readDbToml Data API exposure", () => { withEnv(env), ), ); - - it.effect("an explicit remote api.enabled beats SUPABASE_API_ENABLED", () => { - const ref = "abcdefghijklmnopqrst"; - return withConfig( - ["[remotes.prod]", `project_id = "${ref}"`, "[remotes.prod.api]", "enabled = false"].join( - "\n", - ), - ).pipe( - Effect.flatMap((dir) => readRef(dir, ref)), - Effect.tap((values) => Effect.sync(() => expect(values.exposedSchemas).toEqual([]))), - withEnv({ SUPABASE_API_ENABLED: "true" }), - ); - }); - - it.effect("rejects malformed SUPABASE_API_ENABLED", () => - withConfig("[api]\nenabled = true").pipe( - Effect.flatMap(read), - Effect.exit, - Effect.tap((exit) => - Effect.sync(() => { - expect(Exit.isFailure(exit)).toBe(true); - if (Exit.isFailure(exit)) { - expect(Cause.pretty(exit.cause)).toContain("DbConfigLoadError"); - expect(Cause.pretty(exit.cause)).toContain("invalid api.enabled"); - } - }), - ), - withEnv({ SUPABASE_API_ENABLED: "perhaps" }), - ), - ); }); diff --git a/apps/cli/src/command-internal/db-config.types.ts b/apps/cli/src/command-internal/db-config.types.ts index 147066dad2..4b48cd7364 100644 --- a/apps/cli/src/command-internal/db-config.types.ts +++ b/apps/cli/src/command-internal/db-config.types.ts @@ -1,59 +1,7 @@ import type { Option } from "effect"; -import type { ResolvedRemoteTarget } from "../config/remote-target.service.ts"; import type { PgConnInput } from "./db-connection.service.ts"; -import type { DbConnType } from "./db-target-flags.ts"; - -/** - * The connection-resolution flags shared by `db lint`, `db advisors`, and `test db`. - * - * `connType` records which selector flag the user explicitly set, derived from raw argv via - * `resolveDbTargetFlags`: - * - "db-url" → `--db-url` was set (read `dbUrl.value`) - * - "linked" → `--linked` was set (Management API path) - * - "local" → `--local` was set (explicit local path) - * - undefined → no selector was set; resolver defaults to local - * - * `--db-url` / `--linked` / `--local` are mutually exclusive. `dnsResolver` carries the global - * `--dns-resolver` value, used when the resolver opens its own remote connection (the linked - * pooler temp-role probe); the handler passes the same value to its primary `connect`. - */ -export interface DbConfigFlags { - readonly dbUrl: Option.Option; - readonly connType: DbConnType | undefined; - readonly dnsResolver: "native" | "https"; - /** - * Whether config resolution should decrypt and materialize `[db.vault]` values. - * Defaults to true; `db push --skip-vault` is the only caller that disables it. - */ - readonly resolveVaultSecrets?: boolean; - /** - * The `--password`/`-p` flag value. When `Some`, it takes precedence over the - * `SUPABASE_DB_PASSWORD` env var on the linked path. Commands without a `--password` flag - * (e.g. `test db`) omit it, and the resolver falls back to env only. - */ - readonly password?: Option.Option; - /** - * Optional explicit linked project ref override, letting e.g. `gen types --project-id ` - * reuse the linked resolver's temp-role and pooler fallback without the workdir being linked. - * Shares only `SUPABASE_PROJECT_ID`'s ref-resolution precedence (flag > env > - * the link) — it never drives the local container id or pg-delta project id the way - * that env var does. `None` preserves the normal `--linked` fallback. - */ - readonly linkedProjectRef?: Option.Option; - /** - * The target the caller already resolved through `RemoteTarget`. The `--linked` path uses it - * as is instead of resolving again, and takes the saved pooler URL from it. - */ - readonly target?: ResolvedRemoteTarget; - /** - * Marks `linkedProjectRef` as an ad-hoc remote target (e.g. `gen types --project-id `) - * rather than the current linked workdir, so the resolver must not reuse workdir-scoped - * credentials or cached state for it: it ignores the ambient `SUPABASE_DB_PASSWORD` and skips - * the saved pooler URL, fetching pooler config from the Management API instead. Absent - * for the normal `--linked` path, which may reuse both. - */ - readonly adHocProjectRef?: boolean; -} +import type { HostedMutationConsent } from "./hosted-consent/hosted-consent.token.ts"; +import type { ResolvedTarget } from "./target/resolve-target.ts"; /** * A resolved Postgres connection plus whether it points at the local stack. `isLocal` decides @@ -65,9 +13,26 @@ export interface ResolvedDbConfig { /** Schemas the local config exposes through the Data API; set on the `--local` and `--db-url` paths. */ readonly exposedSchemas?: ReadonlyArray; /** - * The resolved linked project ref (`--linked` path only; `None` for `--local` / `--db-url`). + * The resolved project ref (hosted targets only; `None` for a local or database URL target). * Lets the caller re-read config with the ref applied so a matching `[remotes.]` block can * override fields like `db.major_version` for the container image. */ readonly ref?: Option.Option; } + +/** What {@link DbConfigResolver.resolveConnection} needs: the target and how to authenticate. */ +export interface DbConnectionInput { + readonly target: ResolvedTarget; + readonly dnsResolver: "native" | "https"; + /** Rejected unless the target is the linked project. */ + readonly password: Option.Option; + readonly resolveVaultSecrets?: boolean; + /** Set when the command binds `--password`, so the withheld-password notice can name it. */ + readonly hasPasswordFlag?: boolean; + /** A `--yes`-sourced `Some` lets the retry clear bans; else `LinkedDbNetworkBannedError`. */ + readonly networkBanRemoval: Option.Option; +} + +export interface ResolvedDbConnection extends ResolvedDbConfig { + readonly target: ResolvedTarget; +} diff --git a/apps/cli/src/command-internal/db-connection.service.ts b/apps/cli/src/command-internal/db-connection.service.ts index abba632ff3..e743401536 100644 --- a/apps/cli/src/command-internal/db-connection.service.ts +++ b/apps/cli/src/command-internal/db-connection.service.ts @@ -143,7 +143,7 @@ export interface QueryResult { readonly fields: ReadonlyArray; /** * Postgres type OID per column. Lets the table/CSV formatter render `float4`/`float8` columns - * with `%g`-style formatting while integer columns stay plain. Optional so other `queryRaw` + * in shortest round-trip form (exponent notation for very large or small magnitudes) while integer columns stay plain. Optional so other `queryRaw` * callers/mocks need not set it. */ readonly fieldTypeIds?: ReadonlyArray; diff --git a/apps/cli/src/command-internal/db-connection.sql-pg.integration.test.ts b/apps/cli/src/command-internal/db-connection.sql-pg.integration.test.ts index d841c1a303..4f4d5264cf 100644 --- a/apps/cli/src/command-internal/db-connection.sql-pg.integration.test.ts +++ b/apps/cli/src/command-internal/db-connection.sql-pg.integration.test.ts @@ -468,35 +468,33 @@ describe("dbConnectionLayer connect failures", () => { }), ); - it.live( - "reproduces pgconn's server-error rendering for an auth failure and suggests SUPABASE_DB_PASSWORD", - () => - Effect.gen(function* () { - const server = yield* fakePostgresServer((socket) => { - socket.write( - errorResponse({ - S: "FATAL", - V: "FATAL", - C: "28P01", - M: 'password authentication failed for user "postgres"', - F: "auth.c", - L: "326", - R: "auth_failed", - }), - ); - socket.end(); - }); - const error = yield* connectFailure({ port: server.port }).pipe( - Effect.ensuring(Effect.sync(server.close)), - ); - expect(error.message).toBe( - "failed to connect to postgres: failed to connect to `host=127.0.0.1 user=postgres database=postgres`: " + - 'server error (FATAL: password authentication failed for user "postgres" (SQLSTATE 28P01))', + it.live("renders the server error for an auth failure and suggests SUPABASE_DB_PASSWORD", () => + Effect.gen(function* () { + const server = yield* fakePostgresServer((socket) => { + socket.write( + errorResponse({ + S: "FATAL", + V: "FATAL", + C: "28P01", + M: 'password authentication failed for user "postgres"', + F: "auth.c", + L: "326", + R: "auth_failed", + }), ); - expect(error.suggestion).toBe(SUGGEST_ENV_VAR); - expect(error.message).not.toContain(SENTINEL_PASSWORD); - expect(error.retryable).toBeUndefined(); - }), + socket.end(); + }); + const error = yield* connectFailure({ port: server.port }).pipe( + Effect.ensuring(Effect.sync(server.close)), + ); + expect(error.message).toBe( + "failed to connect to postgres: failed to connect to `host=127.0.0.1 user=postgres database=postgres`: " + + 'server error (FATAL: password authentication failed for user "postgres" (SQLSTATE 28P01))', + ); + expect(error.suggestion).toBe(SUGGEST_ENV_VAR); + expect(error.message).not.toContain(SENTINEL_PASSWORD); + expect(error.retryable).toBeUndefined(); + }), ); it.live( @@ -521,7 +519,7 @@ describe("dbConnectionLayer connect failures", () => { describe("dbConnectionLayer exec failures", () => { it.live( - "maps a real wire ErrorResponse to pgconn's PgError rendering with detail and position", + "maps a real wire ErrorResponse to the server-error rendering with detail and position", () => // Tripwire for the server-error extraction: drives the real driver stack (node-postgres // wire parsing → `DatabaseError` → `@effect/sql-pg`'s `SqlError` cause chain → diff --git a/apps/cli/src/command-internal/db-connection.sql-pg.layer.ts b/apps/cli/src/command-internal/db-connection.sql-pg.layer.ts index fa99bc7109..7f761f07c5 100644 --- a/apps/cli/src/command-internal/db-connection.sql-pg.layer.ts +++ b/apps/cli/src/command-internal/db-connection.sql-pg.layer.ts @@ -8,12 +8,12 @@ import { ConnectionError, SqlError } from "effect/sql/SqlError"; import * as Pg from "pg"; import { to as pgCopyTo } from "pg-copy-streams"; import { - SUGGEST_LOCAL_STACK, connectFailureMessage, connectSuggestion, isDialFailure, isIPv6ConnectivityErrorCause, isSqlState, + localStackSuggestion, } from "./connect-errors.ts"; import { DbConnectError, DbCopyError, DbExecError } from "./db-connection.errors.ts"; import { makePoolSqlClient } from "./db-connection.pool-client.ts"; @@ -199,13 +199,14 @@ export function batchFailureError( error: Error, batch: { readonly completed: number; readonly outcome: BatchOutcome } | undefined, isLocal: boolean, + linkedCheckout?: boolean, ): DbExecError | DbConnectError { if (batch === undefined || batch.outcome === "unsent") { return new DbConnectError({ message: `${BATCH_CONNECTION_LOST}: ${error.message}`, // The checkout failure a tick earlier carries this same hint, so losing the // connection mid-batch must not silently drop it. - ...(isLocal ? { suggestion: SUGGEST_LOCAL_STACK } : {}), + ...(isLocal ? { suggestion: localStackSuggestion(linkedCheckout) } : {}), }); } const mapped = toExecError(error); @@ -963,7 +964,16 @@ const connect = ( resume(Effect.void); return; } - resume(Effect.fail(batchFailureError(error, batchQuery, options.isLocal))); + resume( + Effect.fail( + batchFailureError( + error, + batchQuery, + options.isLocal, + cfg.suggestionContext?.linkedCheckout, + ), + ), + ); }; batchQuery = new PgBatchQuery(statements, finish); try { @@ -1036,7 +1046,7 @@ const connect = ( return { fields: result.fields.map((field) => field.name), // Surface the column type OIDs so the table/CSV formatter can render float4/float8 - // with %g-style formatting while integer columns stay plain. + // in shortest round-trip form while integer columns stay plain. fieldTypeIds: result.fields.map((field) => field.dataTypeID), rows: result.rows, commandTag, diff --git a/apps/cli/src/command-internal/db-connection.sql-pg.unit.test.ts b/apps/cli/src/command-internal/db-connection.sql-pg.unit.test.ts index 0f8156f627..500139e65e 100644 --- a/apps/cli/src/command-internal/db-connection.sql-pg.unit.test.ts +++ b/apps/cli/src/command-internal/db-connection.sql-pg.unit.test.ts @@ -79,7 +79,7 @@ describe("buildConnectionUrl", () => { ); }); - it("forwards runtimeParams as -c flags in the options startup param (Go RuntimeParams)", () => { + it("forwards runtimeParams as -c flags in the options startup param", () => { const url = buildConnectionUrl( { user: "postgres", @@ -138,7 +138,7 @@ describe("sslOptionFor", () => { expect(sslOptionFor(undefined, false, undefined)).toEqual({ rejectUnauthorized: false }); }); - it("treats prefer/require as TLS without verification (their pgconn primary)", () => { + it("treats prefer/require as TLS without verification (their primary mode)", () => { expect(sslOptionFor("prefer", false, undefined)).toEqual({ rejectUnauthorized: false }); expect(sslOptionFor("require", false, undefined)).toEqual({ rejectUnauthorized: false }); }); @@ -154,7 +154,7 @@ describe("sslOptionFor", () => { }); }); - it("verifies the CA chain but skips hostname for verify-ca (pgconn parity)", () => { + it("verifies the CA chain but skips hostname for verify-ca", () => { const ssl = sslOptionFor("verify-ca", false, undefined); expect(ssl).toMatchObject({ rejectUnauthorized: true }); if (typeof ssl === "object" && ssl !== null) { @@ -163,7 +163,7 @@ describe("sslOptionFor", () => { } }); - it("attaches the client cert (cert/key/passphrase) to every TLS mode (pgconn parity)", () => { + it("attaches the client cert (cert/key/passphrase) to every TLS mode", () => { const clientCert = { cert: "CERT", key: "KEY", passphrase: "pw" }; expect(sslOptionFor("verify-full", false, undefined, undefined, clientCert)).toMatchObject({ cert: "CERT", @@ -184,7 +184,7 @@ describe("sslOptionFor", () => { }); }); - it("carries the servername for non-verifying TLS modes too (Go enables sslsni by default)", () => { + it("carries the servername for non-verifying TLS modes too", () => { expect(sslOptionFor("require", false, "db.example.com")).toEqual({ rejectUnauthorized: false, servername: "db.example.com", @@ -206,7 +206,7 @@ describe("sslOptionFor", () => { }); }); -describe("sslConfigsFor (pgconn fallback list)", () => { +describe("sslConfigsFor (fallback list)", () => { it("local connections try a single plaintext (no-TLS) config", () => { expect(sslConfigsFor(undefined, true, undefined)).toEqual([false]); }); @@ -215,7 +215,7 @@ describe("sslConfigsFor (pgconn fallback list)", () => { expect(sslConfigsFor("disable", false, undefined)).toEqual([false]); }); - it("allow is plaintext primary with a TLS fallback ({nil, tlsConfig})", () => { + it("allow is plaintext primary with a TLS fallback (plaintext first, then TLS)", () => { expect(sslConfigsFor("allow", false, undefined)).toEqual([ false, { rejectUnauthorized: false }, @@ -442,17 +442,15 @@ describe("poolStepDownVerify", () => { }), ); - it.effect( - "propagates a failing step-down to the pool callback so the checkout fails (Go AfterConnect parity)", - () => - Effect.gen(function* () { - const failure = new Error("permission denied to set role"); - const client = { query: () => Promise.reject(failure) }; - const done = yield* Effect.callback((resume) => { - poolStepDownVerify(client, (error) => resume(Effect.succeed(error))); - }); - expect(done).toBe(failure); - }), + it.effect("propagates a failing step-down to the pool callback so the checkout fails", () => + Effect.gen(function* () { + const failure = new Error("permission denied to set role"); + const client = { query: () => Promise.reject(failure) }; + const done = yield* Effect.callback((resume) => { + poolStepDownVerify(client, (error) => resume(Effect.succeed(error))); + }); + expect(done).toBe(failure); + }), ); it.effect("wraps a non-Error rejection into an Error for the pool callback", () => @@ -543,14 +541,14 @@ describe("isUnixSocketHost", () => { expect(isUnixSocketHost("::1")).toBe(false); }); - it("treats an uppercase Windows drive path as a socket, lowercase as TCP (pgconn parity)", () => { + it("treats an uppercase Windows drive path as a socket, lowercase as TCP", () => { expect(isUnixSocketHost("C:\\pgsql")).toBe(true); expect(isUnixSocketHost("c:\\pgsql")).toBe(false); expect(isUnixSocketHost("C:")).toBe(false); }); }); -describe("isTerminalConnectError (pgconn fallback termination)", () => { +describe("isTerminalConnectError (fallback termination)", () => { it("terminates on auth/catalog/privilege SQLSTATEs carried on the error cause", () => { // The pg driver attaches the SQLSTATE as `code`; @effect/sql wraps it in `cause`. expect(isTerminalConnectError({ cause: { code: "28P01" } }, false)).toBe(true); @@ -558,7 +556,7 @@ describe("isTerminalConnectError (pgconn fallback termination)", () => { expect(isTerminalConnectError({ code: "42501" }, false)).toBe(true); }); - it("gates 28000 on the attempt having used TLS (pgconn fc.TLSConfig != nil)", () => { + it("gates 28000 on the attempt having used TLS (a TLS attempt is required)", () => { expect(isTerminalConnectError({ code: "28000" }, true)).toBe(true); expect(isTerminalConnectError({ code: "28000" }, false)).toBe(false); }); @@ -591,7 +589,7 @@ describe("toExecError (pg server-error extraction)", () => { }), }); - it("renders pgconn's PgError message and carries code, detail, and position", () => { + it("renders the server error message and carries code, detail, and position", () => { const error = toExecError( sqlErrorChain({ message: 'type "ltree" does not exist', @@ -607,7 +605,7 @@ describe("toExecError (pg server-error extraction)", () => { expect(error.position).toBe(25); }); - it("omits detail and position when the server sent none (Go's non-empty gates)", () => { + it("omits detail and position when the server sent none", () => { const error = toExecError( sqlErrorChain({ message: 'syntax error at or near "BOOM"', @@ -793,6 +791,19 @@ describe("batchFailureError", () => { expect("suggestion" in remote).toBe(false); }); + it("adds the --remote pointer to the local-stack hint on a linked checkout", () => { + const error = batchFailureError( + new Error("socket died"), + { completed: 0, outcome: "unsent" }, + true, + true, + ); + + expect(error).toMatchObject({ + suggestion: `${SUGGEST_LOCAL_STACK}\nTo use the linked project instead, pass --remote.`, + }); + }); + it("keeps a partially written batch on the statement path, blaming statement 0", () => { // A poisoned batch is corked, so `completed` stays 0 and statement 0 is blamed. const error = batchFailureError( diff --git a/apps/cli/src/command-internal/db-image.ts b/apps/cli/src/command-internal/db-image.ts index 29ef955f5e..aa663df918 100644 --- a/apps/cli/src/command-internal/db-image.ts +++ b/apps/cli/src/command-internal/db-image.ts @@ -1,4 +1,4 @@ -import { legacyProjectStatePaths } from "./legacy-project-state.paths.ts"; +import { legacyProjectStatePaths } from "../shared/config/legacy-project-state.paths.ts"; import { usesStackState } from "./stack-backend.ts"; import { Effect, type FileSystem, type Path } from "effect"; import { dockerfileServiceImageRaw } from "../shared/services/dockerfile-images.ts"; diff --git a/apps/cli/src/command-internal/db-image.unit.test.ts b/apps/cli/src/command-internal/db-image.unit.test.ts index 9ce7fc6fad..19eef68a28 100644 --- a/apps/cli/src/command-internal/db-image.unit.test.ts +++ b/apps/cli/src/command-internal/db-image.unit.test.ts @@ -71,7 +71,7 @@ describe("resolveDbImage", () => { }).pipe(Effect.scoped, Effect.provide(BunServices.layer)), ); - it.effect("rewrites to the OrioleDB image on a 15/17 project (Go config.Validate)", () => + it.effect("rewrites to the OrioleDB image on a 15/17 project", () => Effect.gen(function* () { const dir = yield* withTemp; // > 15.1.1.13 → `-orioledb` diff --git a/apps/cli/src/command-internal/db-password-rejection.integration.test.ts b/apps/cli/src/command-internal/db-password-rejection.integration.test.ts new file mode 100644 index 0000000000..dc052493e1 --- /dev/null +++ b/apps/cli/src/command-internal/db-password-rejection.integration.test.ts @@ -0,0 +1,175 @@ +import { BunServices } from "@effect/platform-bun"; +import { describe, expect, it } from "@effect/vitest"; +import { Cause, Effect, Exit, FileSystem, Layer, Path } from "effect"; +import { CliOutput, Command } from "effect/cli"; + +import { rootCommandForFeatures } from "../cli/root.ts"; +import { CliArgs } from "../shared/cli/cli-args.service.ts"; +import { legacyProjectStateLayer } from "../shared/config/legacy-project-state.layer.ts"; +import { textCliOutputFormatter } from "../shared/output/text-formatter.ts"; +import { walkCommandTree, type WalkedCommand } from "../../tests/helpers/command-tree.ts"; +import { + mockAnalytics, + mockOutput, + mockProcessControl, + mockRuntimeInfo, + mockTelemetryRuntime, + mockTty, + processEnvLayer, +} from "../../tests/helpers/mocks.ts"; +import { choiceKeysOf } from "../docs/docs-introspection.ts"; +import { unwrapParam } from "./param-introspection.ts"; + +const root = rootCommandForFeatures(); + +const hasFlag = (entry: WalkedCommand, name: string) => + entry.flags.some((flag) => flag.name === name); + +const isTombstone = (entry: WalkedCommand) => + entry.command.description?.startsWith("Removed:") === true; + +const passwordCommands = walkCommandTree(root).filter( + (entry) => hasFlag(entry, "password") && !isTombstone(entry), +); + +type Selector = "db-url" | "remote-url" | "local" | "none"; + +const withSelector = (selector: Exclude) => + passwordCommands.filter((entry) => hasFlag(entry, selector === "local" ? "local" : "db-url")); + +const requiredFlagArgs = (entry: WalkedCommand): ReadonlyArray => + entry.flags.flatMap((flag) => { + const unwrapped = unwrapParam(flag.param); + const required = + unwrapped !== undefined && + !unwrapped.isOptional && + !(unwrapped.isVariadic && unwrapped.variadicMin === 0) && + unwrapped.single.primitiveType._tag !== "Boolean"; + if (!required) return []; + const [firstChoice] = choiceKeysOf(unwrapped.single.primitiveType) ?? []; + return [`--${flag.name}`, firstChoice ?? "value"]; + }); + +const DIRECT_URL = "postgres://user:secret@127.0.0.1:1/postgres"; + +const selectorArgs = (selector: Selector) => { + switch (selector) { + case "db-url": + return ["--db-url", DIRECT_URL]; + case "remote-url": + return [`--remote=${DIRECT_URL}`]; + case "local": + return ["--local"]; + case "none": + return []; + } +}; + +const runCommand = ( + entry: WalkedCommand, + selector: Selector, + options: { readonly linked?: boolean } = {}, +) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const home = yield* fs.makeTempDirectoryScoped({ prefix: "supabase-password-rejection-" }); + if (options.linked === true) { + yield* fs.makeDirectory(path.join(home, "supabase", ".temp"), { recursive: true }); + yield* fs.writeFileString( + path.join(home, "supabase", ".temp", "project-ref"), + "abcdefghijklmnopqrst", + ); + } + const args = [ + ...entry.path, + ...requiredFlagArgs(entry), + "--password", + "from-flag", + ...selectorArgs(selector), + "--workdir", + home, + // `db pull` rejects `--experimental` without `--declarative` before it validates the password. + ...(entry.path.join(" ") === "db pull" ? [] : ["--experimental"]), + ]; + const exit = yield* Command.runWith(root, { version: "0.0.0-test" })(args).pipe( + Effect.provide( + Layer.mergeAll( + CliOutput.layer(textCliOutputFormatter()), + Layer.succeed(CliArgs, { args }), + mockOutput({ format: "text" }).layer, + BunServices.layer, + legacyProjectStateLayer.pipe(Layer.provide(BunServices.layer)), + mockRuntimeInfo(), + mockAnalytics().layer, + mockTelemetryRuntime(), + mockTty(), + mockProcessControl().layer, + processEnvLayer({ SUPABASE_HOME: home }), + ), + ), + Effect.exit, + ); + return Exit.isFailure(exit) ? Cause.squash(exit.cause) : undefined; + }).pipe(Effect.scoped, Effect.provide(BunServices.layer)); + +describe("--password with a direct database target", () => { + it("is discovered on commands that accept a direct target", () => { + expect(passwordCommands.length).toBeGreaterThan(5); + expect(withSelector("db-url").length).toBeGreaterThan(5); + expect(withSelector("remote-url").length).toBeGreaterThan(5); + expect(withSelector("local").length).toBeGreaterThan(5); + }); + + for (const selector of ["db-url", "remote-url", "local"] as const) { + for (const entry of withSelector(selector)) { + it.effect( + `rejects ${entry.path.join(" ")} --password with ${selector === "remote-url" ? "--remote=" : `--${selector}`}`, + () => + Effect.gen(function* () { + const failure = yield* runCommand(entry, selector); + + expect(failure).toMatchObject({ + _tag: "TargetPasswordConflictError", + message: + selector === "local" + ? "--password only applies to a linked project. The local database uses [db].password from supabase/config.toml." + : "--password can't be used with --remote=. Put the password in the connection string: postgres://USER:PASSWORD@HOST:PORT/postgres", + }); + }), + ); + } + } + + it.effect("rejects migration squash --password when no target is selected", () => + Effect.gen(function* () { + const entry = passwordCommands.find( + (candidate) => candidate.path.join(" ") === "migration squash", + ); + expect(entry).toBeDefined(); + const failure = yield* runCommand(entry!, "none"); + + expect(failure).toMatchObject({ + _tag: "TargetPasswordConflictError", + message: + "--password only applies to a linked project. The local database uses [db].password from supabase/config.toml.", + }); + }), + ); + + it.effect("points a local --password rejection at --remote when the checkout is linked", () => + Effect.gen(function* () { + const entry = passwordCommands.find( + (candidate) => candidate.path.join(" ") === "migration list", + ); + expect(entry).toBeDefined(); + const failure = yield* runCommand(entry!, "local", { linked: true }); + + expect(failure).toMatchObject({ + _tag: "TargetPasswordConflictError", + message: + "--password only applies to a linked project. The local database uses [db].password from supabase/config.toml. To use the linked project instead, pass --remote.", + }); + }), + ); +}); diff --git a/apps/cli/src/command-internal/db-pull-run.errors.ts b/apps/cli/src/command-internal/db-pull-run.errors.ts index 6717bf6520..0a61e61212 100644 --- a/apps/cli/src/command-internal/db-pull-run.errors.ts +++ b/apps/cli/src/command-internal/db-pull-run.errors.ts @@ -30,8 +30,7 @@ export class DbPullInSyncError extends Data.TaggedError("DbPullInSyncError")<{ /** * Explains the non-zero exit instead of letting `Output.fail` append the generic "Try * rerunning the command with --debug" footer: an in-sync database is a finding, not a failure - * to troubleshoot. See `docs/go-cli-divergences.md` for the established message/exit-code - * contract this preserves. + * to troubleshoot. */ readonly suggestion: string; }> { diff --git a/apps/cli/src/command-internal/db-pull-run.ts b/apps/cli/src/command-internal/db-pull-run.ts index 1fc8f4cbfe..09ed990e67 100644 --- a/apps/cli/src/command-internal/db-pull-run.ts +++ b/apps/cli/src/command-internal/db-pull-run.ts @@ -1,37 +1,28 @@ import { Clock, Effect, FileSystem, Option, Path } from "effect"; import { ChildProcessSpawner } from "effect/process"; -import { - DebugFlag, - DnsResolverFlag, - NetworkIdFlag, - resolveExperimentalWithProjectEnv, - resolveYesWithProjectEnv, -} from "./global-flags.ts"; -import { CliArgs } from "../shared/cli/cli-args.service.ts"; +import { DebugFlag, DnsResolverFlag, NetworkIdFlag } from "./global-flags.ts"; import { Output } from "../shared/output/output.service.ts"; import { RuntimeInfo } from "../shared/runtime/runtime-info.service.ts"; import { CommandSettings } from "../config/command-settings.service.ts"; -import { RemoteTarget, type ResolvedRemoteTarget } from "../config/remote-target.service.ts"; +import { CliConfigKeys } from "../config/cli-config-keys.ts"; +import { CliConfigValues } from "../config/cli-config-values.service.ts"; import { bold } from "./colors.ts"; -import { promptYesNo } from "./prompt-yes-no.ts"; import { ipv6Suggestion, isIPv6ConnectivityError } from "./connect-errors.ts"; import { DbConfigResolver } from "./db-config.service.ts"; +import { + hostedMutationPreapproval, + requireTargetMutationConsent, +} from "./hosted-consent/hosted-consent.ts"; +import type { HostedMutationConsent } from "./hosted-consent/hosted-consent.token.ts"; +import type { ResolvedTarget } from "./target/resolve-target.ts"; +import { flagsForTarget } from "./target/target-flags.ts"; import { resolveDbImage } from "./db-image.ts"; import { DbConnection, type PgConnInput } from "./db-connection.service.ts"; -import { - loadProjectEnv, - readDbToml, - resolveDeclarativeDir, - warnDeprecatedOrioleDBVersionEnv, -} from "./db-config.toml-read.ts"; -import type { DbConnType } from "./db-target-flags.ts"; +import { loadProjectEnvValues, readDbToml, resolveDeclarativeDir } from "./db-config.toml-read.ts"; import { makeDir } from "./make-dir.ts"; import { toPostgresURL } from "./postgres-url.ts"; -import { - buildLocalDbContainerInputs, - type LocalDbContainerInputs, -} from "./db-bootstrap/local-container-inputs.ts"; +import { buildLocalDbContainerInputs } from "./db-bootstrap/local-container-inputs.ts"; import { withShadowDatabase } from "./db-bootstrap/shadow-cache.ts"; import { shadowRunInputFromLocalContainerInputs } from "./db-bootstrap/shadow-database.ts"; import { LinkedProjectCache } from "../telemetry/linked-project-cache.service.ts"; @@ -41,13 +32,7 @@ import { warnPreservedUnmanagedDeclarativeFiles, writeDeclarativeSchemas, } from "../commands/db/shared/pgdelta.write.ts"; -import { - parseBoolEnv, - resolveDeclarativeFromArgs, - resolvePullDiffEngine, - schemaPathsTransitionWarning, - shouldUsePgDelta, -} from "./diff-engine.ts"; +import { resolvePullDiffEngine, schemaPathsTransitionWarning } from "./diff-engine.ts"; import { diffMigra } from "../commands/db/shared/migra.ts"; import { writePgDeltaMigrations } from "../commands/db/shared/pgdelta-migrations.write.ts"; import { type DumpOptions, buildSchemaDumpEnv } from "./pg-dump.env.ts"; @@ -59,6 +44,7 @@ import { import { emitPoolerFallbackWarning, isDirectLinkedHost, + orNoPoolerFallback, runWithPoolerFallback, } from "../commands/db/shared/pooler-fallback.ts"; import { dumpSchemaScript } from "./pg-dump.scripts.ts"; @@ -68,19 +54,14 @@ import { PgDeltaEngine, type PgDeltaDatabaseEndpoint, } from "../commands/db/shared/pgdelta-engine.service.ts"; -import { type PgDeltaContext, isPgDeltaDebugEnabled, resolvePgDeltaProjectId } from "./pgdelta.ts"; +import { type PgDeltaContext, isPgDeltaDebugEnabled } from "./pgdelta.ts"; import { prepareShadowSource } from "../commands/db/shared/shadow-source.ts"; import { currentStackBackend, usesStackState } from "./stack-backend.ts"; import { selectionForRequest } from "./stack-project-selection.ts"; import { stackRejectNativeDockerDiffEngine } from "./stack-local-database.ts"; import { stackPrepareShadowSource, stackWithShadowDatabase } from "./stack-shadow.ts"; -import type { DbPullFlags } from "../commands/db/pull/pull.command.ts"; -import { - DbPullDumpError, - DbPullEngineConflictError, - DbPullTargetFlagsError, - DbPullWriteError, -} from "../commands/db/pull/pull.errors.ts"; +import type { DbPullFlags as DbPullCommandFlags } from "../commands/db/pull/pull.command.ts"; +import { DbPullDumpError, DbPullWriteError } from "../commands/db/pull/pull.errors.ts"; import { DbPullInSyncError, DbPullMigrationConflictError } from "./db-pull-run.errors.ts"; import { listRemoteMigrations, @@ -89,19 +70,19 @@ import { } from "./migration-history.ts"; import { updateMigrationHistory } from "../commands/db/pull/pull.sync.ts"; -// `DbPullFlags` is `runDbPull`'s own parameter type, defined alongside the standalone -// `db pull` command that also constructs it — re-exported here so an in-process caller -// (`pull`) can build one without reaching into `commands/db/**` directly. -export type { DbPullFlags }; - -// Established output contract; ends with a `.`. -const DEPRECATION_LINE = - "Flag --use-pg-delta is deprecated and will be removed in v3, use --declarative with [experimental.pgdelta] enabled = true in your config.toml instead."; +/** + * `runDbPull`'s parameter type: the `db pull` command flags without the tombstoned + * `--use-pg-delta` and the target selectors, so an in-process caller (`pull`) can build one + * without reaching into `commands/db/**` directly. + */ +export type DbPullRunFlags = Omit< + DbPullCommandFlags, + "usePgDelta" | "remote" | "dbUrl" | "linked" | "local" | "projectRef" +>; /** * Explains the in-sync non-zero exit instead of the generic "Try rerunning the command with - * --debug…" footer, which would read like a crash for what is really a finding. See - * `docs/go-cli-divergences.md` for the established message/exit-code contract. + * --debug…" footer, which would read like a crash for what is really a finding. */ const IN_SYNC_SUGGESTION = "The remote database is already in sync with your local migrations — nothing to pull."; @@ -109,30 +90,14 @@ const IN_SYNC_SUGGESTION = /** Migration-file mode for the initial pg_dump seed. */ const MIGRATION_FILE_MODE = 0o644; -// `--experimental` without `--declarative` is a deprecated path: the in-process declarative -// export covers the same per-object-files outcome. Printed only when the experimental gate -// selected this branch, not when `--declarative` already did. -const EXPERIMENTAL_STRUCTURED_DUMP_DEPRECATION_LINE = - "The --experimental structured-dump mode for `db pull` is deprecated and will be removed in v3. Use --declarative instead to pull the remote schema as per-object files."; - export type DbPullInvoke = { - /** Skip printing `Finished supabase db pull.` for callers that must not emit it. */ - readonly skipFinishedLine?: boolean; /** - * Overrides `--yes`/`SUPABASE_YES`/`supabase/.env` resolution for an - * in-process caller that already ran its own aggregated confirmation. - * `undefined` keeps the existing resolution. + * The consent an in-process caller already collected for the remote history update; `Some` + * skips this run's own question. */ - readonly assumeYes?: boolean; - /** - * Forces migration mode regardless of the ambient `--experimental`/`SUPABASE_EXPERIMENTAL` - * gate, for an in-process caller (`pull`) whose dirty-guard, confirmation message, and - * SIDE_EFFECTS.md all assume this step only ever writes into `supabase/migrations`. Without - * this override, an ambient experimental flag would silently switch to the declarative export - * path, writing `supabase/schemas/**` and potentially `config.toml`'s `schema_paths` - * unguarded. `undefined` keeps the existing resolution. - */ - readonly forceMigrationMode?: boolean; + readonly preapproved?: Option.Option; + /** Project `.env*` values the caller already loaded; `undefined` loads them here. */ + readonly projectEnv?: Record; }; export type DbPullOutcome = @@ -149,13 +114,14 @@ export type DbPullOutcome = }; /** - * Runs `db pull`'s target resolution and pull (declarative or migration mode) - * without emitting the final `output.success`/"Finished" line — the caller - * (`dbPull` for the standalone command, or an in-process orchestrator) - * owns that emission based on the returned {@link DbPullOutcome}. + * Runs `db pull` against an already-resolved target (declarative or migration mode) without + * emitting the final `output.success`/"Finished" line — the caller (`dbPull` for the + * standalone command, or an in-process orchestrator) owns that emission based on the returned + * {@link DbPullOutcome}. */ export const runDbPull = Effect.fn("db.pull.run")(function* ( - flags: DbPullFlags, + flags: DbPullRunFlags, + target: ResolvedTarget, invoke?: DbPullInvoke, ) { const output = yield* Output; @@ -169,152 +135,72 @@ export const runDbPull = Effect.fn("db.pull.run")(function* ( const path = yield* Path.Path; const dnsResolver = yield* DnsResolverFlag; const debug = yield* DebugFlag; - const cliArgs = yield* CliArgs; + const configValues = yield* CliConfigValues; - // `--yes` or `SUPABASE_YES`. The project `.env` is loaded before the migration - // history prompt, so a `SUPABASE_YES` set only in `supabase/.env` auto-confirms - // the native initial-migra history repair too. - const projectEnv = yield* loadProjectEnv(fs, path, cliSettings.workdir); - const yes = yield* resolveYesWithProjectEnv(projectEnv); - // `EXPERIMENTAL` resolves from either the global `--experimental` flag or - // `SUPABASE_EXPERIMENTAL`, reusing `resolveExperimentalWithProjectEnv`'s flag-over-env - // precedence instead of re-deriving it. Resolved once, same as `yes` above; declarative mode - // ignores it. `invoke?.forceMigrationMode` overrides this for an in-process caller that must - // never take the declarative export path — see {@link DbPullInvoke.forceMigrationMode}. - const experimental = invoke?.forceMigrationMode - ? false - : yield* resolveExperimentalWithProjectEnv(projectEnv); + const projectEnv = + invoke?.projectEnv ?? (yield* loadProjectEnvValues(fs, path, cliSettings.workdir)); + const preapproved = invoke?.preapproved ?? Option.none(); - let linkedRefForCache: string | undefined; + const connType = target.kind; + // `--local`/`--db-url` never merge a `[remotes.*]` block, so only a linked target has a ref. + const linkedRef = target.kind === "linked" ? target.ref : undefined; return yield* Effect.gen(function* () { const name = Option.getOrElse(flags.name, () => "remote_schema"); - // `--declarative` and the deprecated `--use-pg-delta` both bind to the same - // `useDeclarative` outcome. When both are passed, the last occurrence in argv wins - // (e.g. `--declarative --use-pg-delta=false` => migration mode); since parsed Options don't - // carry order, the both-present case replays that rule off the raw argv instead of ORing the - // two, which would diverge on conflicting values. - const useDeclarative = - Option.isSome(flags.declarative) && Option.isSome(flags.usePgDelta) - ? (resolveDeclarativeFromArgs(cliArgs.args) ?? false) - : Option.getOrElse(flags.declarative, () => false) || - Option.getOrElse(flags.usePgDelta, () => false); - if (Option.isSome(flags.usePgDelta)) { - yield* output.raw(`${DEPRECATION_LINE}\n`, "stderr"); - } - // Deprecated `--experimental` dump: same in-process export as `--declarative`. - const useExperimentalExport = experimental && !useDeclarative; - if (useExperimentalExport) { - yield* output.raw(`${EXPERIMENTAL_STRUCTURED_DUMP_DEPRECATION_LINE}\n`, "stderr"); - } - const useDeclarativeExport = useDeclarative || useExperimentalExport; - - // Mutually exclusive flag groups: `[db-url linked local]`, `[declarative - // diff-engine]`, `[use-pg-delta diff-engine]`. "set" means the flag was - // explicitly passed. - const targetSet: Array = []; - if (Option.isSome(flags.dbUrl)) targetSet.push("db-url"); - if (Option.isSome(flags.linked)) targetSet.push("linked"); - if (Option.isSome(flags.local)) targetSet.push("local"); - if (targetSet.length > 1) { - return yield* new DbPullTargetFlagsError({ - message: `if any flags in the group [db-url linked local] are set none of the others can be; [${[...targetSet].sort().join(" ")}] were all set`, - }); - } - for (const [flagName, present] of [ - ["declarative", Option.isSome(flags.declarative)], - ["use-pg-delta", Option.isSome(flags.usePgDelta)], - ] as const) { - if (present && Option.isSome(flags.diffEngine)) { - return yield* new DbPullEngineConflictError({ - message: `if any flags in the group [${flagName} diff-engine] are set none of the others can be; [${[flagName, "diff-engine"].sort().join(" ")}] were all set`, - }); - } - } + const useDeclarative = Option.getOrElse(flags.declarative, () => false); - const connType: DbConnType = Option.isSome(flags.dbUrl) - ? "db-url" - : Option.isSome(flags.local) - ? "local" - : "linked"; yield* Effect.annotateCurrentSpan({ "db.conn_type": connType, - "db.pull.mode": useDeclarativeExport ? "declarative" : "migration", + "db.pull.mode": useDeclarative ? "declarative" : "migration", }); - // `--project-ref` never implies `--linked` and must not be silently - // discarded on a non-linked target — see push.handler.ts's identical guard - // for the full TS-only rationale. - if (Option.isSome(flags.projectRef) && connType !== "linked") { - return yield* new DbPullTargetFlagsError({ - message: - "--project-ref only applies when targeting the linked project; use it with --linked (not --local or --db-url)", - }); - } - - // Pre-load the ref and re-read config here, before `resolver.resolve()` below, so the - // override print and merged-config validation happen before the connection attempt: a - // `resolve()` failure (bad password, unreachable host, network-ban lookup, …) must still - // tell the user which `[remotes.*]` block matched. `--local`/`--db-url` never merge a - // remote block, so only the linked path pre-resolves a ref. - let linkedRef: string | undefined; - let linkedTarget: ResolvedRemoteTarget | undefined; - if (connType === "linked") { - const remoteTarget = yield* RemoteTarget; - linkedTarget = yield* remoteTarget.load(flags.projectRef); - linkedRef = linkedTarget.ref; - // Cache the ref the moment it's known, not after `toml`/`localInputs` below resolve - // (both fallible): the project cache should still be written even when a later step - // (config validation, connection, the pull itself) fails. - linkedRefForCache = linkedRef; - } + // Read the config for the matching `[remotes.]` block before connecting, so a + // connection failure still reports which override matched. const toml = yield* readDbToml(fs, path, cliSettings.workdir, linkedRef); if (toml.appliedRemote !== undefined) { yield* output.raw(`Loading config override: [remotes.${toml.appliedRemote}]\n`, "stderr"); } - // Without the deprecated export, the shadow inputs' project-context load below prints it. - if (useExperimentalExport) yield* warnDeprecatedOrioleDBVersionEnv(toml.envLookup); + const resolvedConfig = yield* configValues.load({ + workdir: cliSettings.workdir, + projectRef: Option.fromNullishOr(linkedRef), + }); const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const runtimeInfo = yield* RuntimeInfo; const networkIdFlag = yield* NetworkIdFlag; // Validate native shadow inputs before target resolution performs remote side effects. - const localInputs: Option.Option = useExperimentalExport - ? Option.none() - : Option.some( - yield* buildLocalDbContainerInputs( - spawner, - cliSettings.workdir, - networkIdFlag, - runtimeInfo.platform, - debug, - // So the shadow's own container spec reflects the matching `[remotes.]` - // override, same as `toml` above. - connType === "linked" ? linkedRef : undefined, - // `toml`'s remote-override-key tracking (same matched block), so a remote-set - // bootstrap field isn't re-overridden by a conflicting `SUPABASE_*` env var here. - toml.remoteOverrideKeys, - ), - ); + const localInputs = yield* buildLocalDbContainerInputs( + spawner, + cliSettings.workdir, + networkIdFlag, + runtimeInfo.platform, + debug, + // So the shadow's own container spec reflects the matching `[remotes.]` + // override, same as `toml` above. + linkedRef, + ); - const resolved = yield* resolver.resolve({ - dbUrl: flags.dbUrl, - connType, + const connectionInput = { + target, dnsResolver, password: flags.password ?? Option.none(), - linkedProjectRef: flags.projectRef, - target: linkedTarget, - }); - if (linkedRef === undefined) { - linkedRef = Option.getOrUndefined(resolved.ref ?? Option.none()); - } - if (linkedRef !== undefined) linkedRefForCache = linkedRef; + hasPasswordFlag: true, + networkBanRemoval: yield* hostedMutationPreapproval, + }; + const resolved = yield* resolver.resolveConnection(connectionInput); + const requireHistoryConsent = (migrations: number) => + Option.isSome(preapproved) + ? Effect.void + : requireTargetMutationConsent(resolved, { + action: "Update the remote migration history table", + details: [ + `Write ${migrations} migration file${migrations === 1 ? "" : "s"} to supabase/migrations.`, + "Record them as applied in the remote migration history.", + ], + }).pipe(Effect.asVoid); const targetUrl = toPostgresURL(resolved.conn); const ctx: PgDeltaContext = { - // Precedence: `SUPABASE_PROJECT_ID` env override, then config.toml's `project_id`, then - // the workdir basename fallback — with the matched `[remotes.]` block's own - // `project_id` suppressing the raw env argument on the linked path. - projectId: resolvePgDeltaProjectId(path, cliSettings.projectId, toml, cliSettings.workdir), + projectId: (yield* resolvedConfig.get(CliConfigKeys.projectId)).value, cwd: cliSettings.workdir, denoVersion: toml.denoVersion, projectEnv: toml.projectEnv, @@ -344,18 +230,9 @@ export const runDbPull = Effect.fn("db.pull.run")(function* ( }) && isIPv6ConnectivityError(error.message) ) { - // A pooler resolution failure is treated as "no fallback" (re-fail the - // original diff error), not surfaced as its own error. - const pooler = yield* resolver - .resolvePoolerFallback({ - dbUrl: flags.dbUrl, - connType: "linked", - dnsResolver, - password: flags.password ?? Option.none(), - linkedProjectRef: flags.projectRef, - target: linkedTarget, - }) - .pipe(Effect.orElseSucceed(() => Option.none())); + const pooler = yield* orNoPoolerFallback( + resolver.resolvePoolerFallback(connectionInput), + ); if (Option.isSome(pooler)) { yield* emitPoolerFallbackWarning(resolved.conn.host); return yield* attempt({ @@ -376,44 +253,32 @@ export const runDbPull = Effect.fn("db.pull.run")(function* ( engine: Option.getOrElse(flags.diffEngine, () => "migra"), pgDeltaDefault: (yield* currentStackBackend).kind === "stack" || - shouldUsePgDelta({ - configEnabled: toml.pgDelta.enabled, - usePgDeltaFlag: false, - envEnabled: parseBoolEnv(toml.envLookup("SUPABASE_EXPERIMENTAL_PG_DELTA")), - }), + (yield* resolvedConfig.get(CliConfigKeys.experimental.pgdelta.enabled)).value, }); if (Option.getOrElse(flags.diffEngine, () => "pg-delta") === "migra") { yield* stackRejectNativeDockerDiffEngine("--diff-engine migra"); } const diffEngine = usePgDeltaDiff ? "pg-delta" : "migra"; + const pgDeltaDebug = yield* isPgDeltaDebugEnabled; - // Connectivity check, run before dialing. return yield* Effect.scoped( Effect.gen(function* () { - yield* output.raw( - `Connecting to ${resolved.isLocal ? "local" : "remote"} database...\n`, - "stderr", - ); const session = yield* connection.connect(resolved.conn, { isLocal: resolved.isLocal, dnsResolver, }); - // Declarative export path (`--declarative` or deprecated `--experimental`). - if (useDeclarativeExport) { + if (useDeclarative) { yield* output.raw("Preparing declarative schema export using pg-delta...\n", "stderr"); const declarativeDirRel = resolveDeclarativeDir(path, toml.pgDelta); const declarativeDir = path.resolve(cliSettings.workdir, declarativeDirRel); - const pgDeltaDebug = yield* isPgDeltaDebugEnabled; const exportSchema = (target: PgDeltaDatabaseEndpoint) => pgDeltaEngine.exportDeclarativeSchema({ context: ctx, target, schema: flags.schema, formatOptions, - ...(connType === "linked" && linkedRef !== undefined - ? { projectRef: linkedRef } - : {}), + ...(linkedRef !== undefined ? { projectRef: linkedRef } : {}), debug: pgDeltaDebug, strictCoverage: flags.strictCoverage, }); @@ -463,7 +328,7 @@ export const runDbPull = Effect.fn("db.pull.run")(function* ( path, path.join(cliSettings.workdir, "supabase", "migrations"), ); - const sync = reconcileMigrations(remote, local, connType === "local"); + const sync = reconcileMigrations(remote, local, flagsForTarget(target)); if (sync.kind === "conflict") { return yield* new DbPullMigrationConflictError({ message: @@ -481,11 +346,8 @@ export const runDbPull = Effect.fn("db.pull.run")(function* ( // is "in sync", a non-empty dump is a valid initial migration on its own. let seedWroteBytes = false; - // `Option.getOrThrow` is safe here: this point is only reached after the - // declarative-export branch already returned, so `localInputs` was always built. - const pullLocalInputs = Option.getOrThrow(localInputs); - if (seededFromDump) { + yield* requireHistoryConsent(1); yield* makeDir(fs, path.dirname(migrationPath)).pipe( Effect.mapError((cause) => new DbPullWriteError({ message: cause.message })), ); @@ -494,7 +356,7 @@ export const runDbPull = Effect.fn("db.pull.run")(function* ( projectRoot: cliSettings.workdir, majorVersion: toml.majorVersion, orioledbVersion: Option.getOrUndefined(toml.orioledbVersion), - services: pullLocalInputs.context.config.local?.services, + services: localInputs.context.config.local?.services, appliedRemote: toml.appliedRemote, })).database.image : (yield* resolveDbImage( @@ -591,16 +453,7 @@ export const runDbPull = Effect.fn("db.pull.run")(function* ( isLocal: resolved.isLocal, projectHost: cliSettings.projectHost, resolvePooler: () => - resolver - .resolvePoolerFallback({ - dbUrl: flags.dbUrl, - connType: "linked", - dnsResolver, - password: flags.password ?? Option.none(), - linkedProjectRef: flags.projectRef, - target: linkedTarget, - }) - .pipe(Effect.orElseSucceed(() => Option.none())), + orNoPoolerFallback(resolver.resolvePoolerFallback(connectionInput)), runWithConn: runSchemaDump, reprintOnRetry: Effect.void, }); @@ -624,13 +477,13 @@ export const runDbPull = Effect.fn("db.pull.run")(function* ( const stackBackend = (yield* currentStackBackend).kind === "stack"; const resolvedPullShadowImage = stackBackend ? "stack-ephemeral" - : yield* pullLocalInputs.resolvePostgresImage; + : yield* localInputs.resolvePostgresImage; const migrationMode: "legacy" | "pgdelta-next" = usePgDeltaDiff ? "pgdelta-next" : "legacy"; const shadowInput = { ...shadowRunInputFromLocalContainerInputs( - pullLocalInputs, + localInputs, resolvedPullShadowImage, toml, fs, @@ -639,7 +492,7 @@ export const runDbPull = Effect.fn("db.pull.run")(function* ( targetLocal: resolved.isLocal, migrationMode, // `toml.schemaPathPatterns` is used here, not the raw `@supabase/config` field on - // `pullLocalInputs`, since only `toml` (via `readDbToml`) resolves the + // `localInputs`, since only `toml` (via `readDbToml`) resolves the // `SUPABASE_DB_MIGRATIONS_SCHEMA_PATHS` env override. schemaPaths: toml.schemaPathPatterns, pgDelta: toml.pgDelta, @@ -680,7 +533,7 @@ export const runDbPull = Effect.fn("db.pull.run")(function* ( }, schema: diffSchema, formatOptions, - debug: yield* isPgDeltaDebugEnabled, + debug: pgDeltaDebug, strictCoverage: flags.strictCoverage, }); } @@ -738,6 +591,11 @@ export const runDbPull = Effect.fn("db.pull.run")(function* ( }); } + // The initial migra pull asked before its dump wrote the file. + if (!seededFromDump) { + yield* requireHistoryConsent(usePgDeltaDiff ? (diffOutcome.files?.length ?? 0) : 1); + } + // Build the list of migration files to record in the remote history. The // migra engine writes exactly one file (the dump-seeded or freshly written // migrationPath); the pg-delta engine writes one ordered file per @@ -828,34 +686,18 @@ export const runDbPull = Effect.fn("db.pull.run")(function* ( ); } - // Prompt to update the remote migration history table. Returns the default - // (`true`) on `--yes`, on an empty non-interactive stdin, or on any prompt error — - // it never fails the command. - let remoteHistoryUpdated = false; - const updateHistoryTitle = "Update remote migration history table?"; - // `invoke?.assumeYes` overrides this resolution entirely for an in-process - // caller that already ran its own aggregated confirmation. Otherwise, honors - // `--yes`, scans piped stdin on a non-TTY before falling back to the default, - // and otherwise prompts on a real TTY. - const shouldUpdate = - invoke?.assumeYes !== undefined - ? invoke.assumeYes - : yield* promptYesNo(output, yes, updateHistoryTitle, true); - if (shouldUpdate) { - // The migration file(s) in `writtenMigrations` are already on disk at this point, so - // a failure here must still report them as written: re-raise the same - // `DbPullWriteError` `updateMigrationHistory` fails with, carrying their paths. - yield* updateMigrationHistory(session, fs, path, writtenMigrations).pipe( - Effect.mapError( - (cause) => - new DbPullWriteError({ - message: cause.message, - writtenSoFar: writtenMigrations.map((written) => written.path), - }), - ), - ); - remoteHistoryUpdated = true; - } + // The migration file(s) in `writtenMigrations` are already on disk at this point, so + // a failure here must still report them as written: re-raise the same + // `DbPullWriteError` `updateMigrationHistory` fails with, carrying their paths. + yield* updateMigrationHistory(session, fs, path, writtenMigrations).pipe( + Effect.mapError( + (cause) => + new DbPullWriteError({ + message: cause.message, + writtenSoFar: writtenMigrations.map((written) => written.path), + }), + ), + ); return { kind: "migration", @@ -863,17 +705,13 @@ export const runDbPull = Effect.fn("db.pull.run")(function* ( // writes one file per unit); `dbPull`'s emission derives the released `schemaWritten` // string field from its first entry. schemaFiles: writtenMigrations.map((written) => written.path), - remoteHistoryUpdated, + remoteHistoryUpdated: true, engine: diffEngine, } as const; }), ); }).pipe( - Effect.ensuring( - Effect.suspend(() => - linkedRefForCache !== undefined ? linkedProjectCache.cache(linkedRefForCache) : Effect.void, - ), - ), + Effect.ensuring(linkedRef !== undefined ? linkedProjectCache.cache(linkedRef) : Effect.void), Effect.ensuring(telemetryState.flush), ); }); diff --git a/apps/cli/src/command-internal/db-push-core.ts b/apps/cli/src/command-internal/db-push-core.ts index d80305ffb5..b42c8104fe 100644 --- a/apps/cli/src/command-internal/db-push-core.ts +++ b/apps/cli/src/command-internal/db-push-core.ts @@ -1,7 +1,6 @@ -import { Effect, FileSystem, Path } from "effect"; +import { Effect, FileSystem, Option, Path } from "effect"; -import { promptYesNo } from "./prompt-yes-no.ts"; -import { CONTEXT_CANCELED_MESSAGE } from "../shared/output/errors.ts"; +import type { DbSeedPlan } from "./seed-remote-consent.ts"; import { Output } from "../shared/output/output.service.ts"; import { listLocalMigrations } from "./migration-list.ts"; import { @@ -14,7 +13,6 @@ import { import { type SeedFile, getPendingSeeds, seedData } from "./seed-ops.ts"; import { DbPushApplyError, - DbPushCancelledError, DbPushMissingLocalError, DbPushMissingRemoteError, DbPushRolesError, @@ -24,6 +22,8 @@ import type { DbTomlValues } from "./db-config.toml-read.ts"; import { DbConnection, type PgConnInput } from "./db-connection.service.ts"; import { applyMigrations, seedGlobals } from "./migration-apply.ts"; import { listRemoteMigrations, suggestRevertHistory } from "./migration-history.ts"; +import { requireTargetMutationConsent } from "./hosted-consent/hosted-consent.ts"; +import type { HostedMutationConsent } from "./hosted-consent/hosted-consent.token.ts"; import { upsertVaultSecrets } from "./vault.ts"; const CUSTOM_ROLES_PATH = "supabase/roles.sql"; @@ -38,12 +38,19 @@ const confirmSeedAll = (seeds: ReadonlyArray): string => .map((seed) => ` • ${bold(seed.dirty ? `${seed.path} (hash update)` : seed.path)}\n`) .join(""); +const plural = (count: number, noun: string): string => `${count} ${noun}${count === 1 ? "" : "s"}`; + +const joinParts = (parts: ReadonlyArray): string => + parts.length <= 1 ? (parts[0] ?? "") : `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}`; + +const capitalize = (text: string): string => text.charAt(0).toUpperCase() + text.slice(1); + const applyError = (message: string) => new DbPushApplyError({ message }); /** * Everything `db push` does once its target connection and config are already resolved. Callers * (`db push`, `bootstrap`) resolve the project ref, connection, and `config.toml` themselves and - * pass the results in; this core never touches `RemoteTarget`/`DbConfigResolver` itself. + * pass the results in; this core never touches `ProjectRefResolver`/`DbConfigResolver` itself. * * The "DRY RUN: …" heads-up line prints first here, after the caller's own connection-resolution * output (e.g. "Initialising login role..."), so it lives here rather than at either call site. @@ -51,18 +58,12 @@ const applyError = (message: string) => new DbPushApplyError({ message }); export interface DbPushCoreInput { /** Absolute project directory (never read from `CommandSettings.workdir`). */ readonly workdir: string; - /** Resolved project ref, or `""` for `--local` / `--db-url`. */ + /** Resolved project ref, or `""` for a local or database URL target. */ readonly projectRef: string; readonly conn: PgConnInput; readonly isLocal: boolean; - /** - * Whether `--local` (not `--db-url`/`--linked`) was the explicit target selector, distinct - * from `isLocal` (whether the resolved connection happens to point at a local address, e.g. a - * `--db-url` pointing at `127.0.0.1`). Only feeds the "missing local migrations" repair - * suggestion's `--local` flag; `bootstrap` never selects `--local`, so it is always `false` - * there. - */ - readonly repairSuggestsLocalFlag: boolean; + /** Flags that point the "missing local migrations" repair suggestion at the pushed target. */ + readonly repairTargetFlags: string; /** * Gates the "DRY RUN: …" heads-up line, the "Would push/seed/create …" plan, and the JSON * `dryRun` field. @@ -71,12 +72,13 @@ export interface DbPushCoreInput { readonly includeAll: boolean; readonly includeRoles: boolean; readonly includeSeed: boolean; + readonly seed: DbSeedPlan; readonly includeVault: boolean; readonly dnsResolver: "native" | "https"; /** Already loaded + validated `config.toml`, e.g. via `checkDbToml`. */ readonly toml: DbTomlValues; - /** Already resolved confirm-prompt default, e.g. via `resolveYesWithProjectEnv`. */ - readonly yes: boolean; + /** Consent the caller already obtained; `None` asks once after the plan (never for local). */ + readonly preapproved: Option.Option; /** * Standalone `db push` emits a `--output-format` json/stream-json success result for its own * invocation; `bootstrap` suppresses it since it emits its own top-level result instead. @@ -95,7 +97,7 @@ export const dbPushCore = Effect.fn("DbPush.run")(function* (input: DbPushCoreIn projectRef, conn, isLocal, - repairSuggestsLocalFlag, + repairTargetFlags, dryRun, includeAll, includeRoles, @@ -103,8 +105,9 @@ export const dbPushCore = Effect.fn("DbPush.run")(function* (input: DbPushCoreIn includeVault, dnsResolver, toml, - yes, + preapproved, emitStructuredResult, + seed, } = input; const vaultSecrets = toml.vault; @@ -115,12 +118,10 @@ export const dbPushCore = Effect.fn("DbPush.run")(function* (input: DbPushCoreIn yield* output.raw("DRY RUN: migrations will *not* be pushed to the database.\n", "stderr"); } - const databaseName = isLocal ? "local database" : "remote database"; const statusTarget = isLocal ? "Local database" : "Remote database"; yield* Effect.scoped( Effect.gen(function* () { - yield* output.raw(`Connecting to ${isLocal ? "local" : "remote"} database...\n`, "stderr"); const session = yield* dbConn.connect(conn, { isLocal, dnsResolver }); let pending: ReadonlyArray = []; @@ -137,7 +138,7 @@ export const dbPushCore = Effect.fn("DbPush.run")(function* (input: DbPushCoreIn if (result.kind === "missing-local") { return yield* new DbPushMissingLocalError({ message: ERR_MISSING_LOCAL, - suggestion: suggestRevertHistory(result.versions, repairSuggestsLocalFlag), + suggestion: suggestRevertHistory(result.versions, repairTargetFlags), }); } if (result.kind === "missing-remote") { @@ -157,13 +158,13 @@ export const dbPushCore = Effect.fn("DbPush.run")(function* (input: DbPushCoreIn let seeds: ReadonlyArray = []; if (includeSeed) { - if (!toml.seed.enabled) { + if (!seed.input.enabled) { yield* output.raw( `Skipping seed because it is disabled in config.toml for project: ${projectRef}\n`, "stderr", ); } else { - seeds = yield* getPendingSeeds(session, fs, path, toml.seed.sqlPaths, workdir); + seeds = yield* getPendingSeeds(session, fs, path, seed.input.sqlPaths, workdir); } } @@ -216,16 +217,39 @@ export const dbPushCore = Effect.fn("DbPush.run")(function* (input: DbPushCoreIn yield* output.raw(confirmSeedAll(seeds), "stderr"); } } else { - if (globals.length > 0) { - const ok = yield* promptYesNo( - output, - yes, - "Do you want to create custom roles in the database cluster?", - true, + if (Option.isNone(preapproved)) { + const parts = [ + ...(globals.length > 0 ? ["create custom roles"] : []), + ...(pending.length > 0 ? [`apply ${plural(pending.length, "migration")}`] : []), + ...(seeds.length > 0 ? [`seed ${plural(seeds.length, "file")}`] : []), + ]; + const seedDetail = seed.detail(seeds.map((s) => s.path)); + yield* requireTargetMutationConsent( + { conn, isLocal }, + { + action: capitalize(joinParts(parts)), + details: [ + ...(globals.length > 0 ? [`Create custom roles from ${bold(globals[0]!)}`] : []), + ...(pending.length > 0 + ? [ + `Apply these migrations:\n${confirmPushAll(pending.map((p) => path.basename(p))).trimEnd()}`, + ...(includeVault && vaultSecrets.some((secret) => secret.resolved) + ? ["Update vault secrets from config.toml"] + : []), + ] + : []), + ...(seeds.length > 0 + ? [ + `Seed these files:\n${confirmSeedAll(seeds).trimEnd()}`, + ...(seedDetail === undefined ? [] : [capitalize(seedDetail)]), + ] + : []), + ], + }, ); - if (!ok) { - return yield* new DbPushCancelledError({ message: CONTEXT_CANCELED_MESSAGE }); - } + } + + if (globals.length > 0) { yield* seedGlobals( session, fs, @@ -236,15 +260,6 @@ export const dbPushCore = Effect.fn("DbPush.run")(function* (input: DbPushCoreIn } if (pending.length > 0) { - const ok = yield* promptYesNo( - output, - yes, - `Do you want to push these migrations to the ${databaseName}?\n${confirmPushAll(pending.map((p) => path.basename(p)))}`, - true, - ); - if (!ok) { - return yield* new DbPushCancelledError({ message: CONTEXT_CANCELED_MESSAGE }); - } if (includeVault) { yield* upsertVaultSecrets(session, vaultSecrets); } @@ -254,15 +269,6 @@ export const dbPushCore = Effect.fn("DbPush.run")(function* (input: DbPushCoreIn } if (seeds.length > 0) { - const ok = yield* promptYesNo( - output, - yes, - `Do you want to seed the ${databaseName} with these files?\n${confirmSeedAll(seeds)}`, - true, - ); - if (!ok) { - return yield* new DbPushCancelledError({ message: CONTEXT_CANCELED_MESSAGE }); - } yield* seedData(session, fs, workdir, path, seeds, applyError); } else if (includeSeed) { yield* output.raw("Seed files are up to date.\n", "stderr"); diff --git a/apps/cli/src/command-internal/db-target-flags.ts b/apps/cli/src/command-internal/db-target-flags.ts deleted file mode 100644 index 8622d8e393..0000000000 --- a/apps/cli/src/command-internal/db-target-flags.ts +++ /dev/null @@ -1,296 +0,0 @@ -/** - * Pure flag-presence helpers for the `--db-url / --linked / --local` target selection shared by - * `db lint`, `db advisors`, and `test db`. - * - * Effect CLI's parsed flags carry no "was this explicitly set" bit, so this re-derives it from - * raw `process.argv`. The mutually-exclusive-group error's "were all set" list is alphabetically - * sorted regardless of each command's own flag registration order, which `setFlags` always - * returns. A value-consuming flag given in space form (`--flag value`) has its value token - * skipped during the scan, so e.g. `--schema --linked` does not misdetect `--linked` as changed. - */ - -import { GEN_TYPES_LANGUAGE_VALUE_FLAG_NAMES } from "../commands/gen/types/types.languages.ts"; - -export type DbConnType = "db-url" | "linked" | "local"; - -export interface DbTargetSelection { - /** Alphabetically-sorted list of explicitly-set selector flags ("db-url", "linked", "local"). */ - readonly setFlags: ReadonlyArray; - /** - * Changed-first selection: db-url > local > linked (if changed) > undefined (→ local - * default). `undefined` means no selector was explicitly set; callers default to "local". - */ - readonly connType: DbConnType | undefined; -} - -/** - * Long-form flags that consume the next token as their value in space-separated form - * (`--flag value`), so the target-selector scan skips it. Also consulted by - * `extractChangedFlagNames` across every command's argv, so this lists every value-consuming - * flag declared anywhere under `commands/`; a missing entry can misdetect a flag's value token - * as a global flag's long name. `db-target-flags.unit.test.ts` enforces coverage for - * directly-declared flags; flags built through a name-parameterized helper are listed by hand. - */ -export const VALUE_CONSUMING_LONG_FLAGS = new Set([ - // db-family command flags - "db-url", - "password", // db push/pull/dump/remote (short -p) - "sql-paths", - "schema", - "level", - "fail-on", - "type", - // migration/db credential flag; consumes the next token as its value. - "password", - // inspect report flag - "output-dir", - // storage cp command flags (Flag.String / Flag.Int) - "cache-control", - "content-type", - "jobs", - // global flags (Flag.String / Flag.Literals) - "output", - "output-format", - "profile", - "workdir", - "network-id", - "dns-resolver", - "agent", - // `--log-level` is not listed: an argv giving it a flag-shaped value fails the real parse - // before any scanner here runs, so nothing can mis-consume around it. `--completions` prints - // and exits before any handler runs, so these scans never see it either. - // Every other value-consuming flag declared directly across commands/ (see the doc comment - // above). - "add-domains", - "algorithm", - "attribute-mapping-file", - "auth", - "config", - "custom-hostname", - "db-allow-cidr", - "db-password", - "db-unban-ip", - "desired-subdomain", - "diff-engine", - "domains", - "env-file", - "exclude", - "exp", - "exposure", - "file", - "from", - "from-backup", - "git-branch", - "import-map", - "inspect-mode", - "instances", - "kind", - "lang", - "last", - "metadata-file", - "metadata-url", - "name", - "name-id-format", - "notify-url", - "org-id", - "override-name", - "payload", - "plan", - "postgres-engine", - "project-id", - "project-ref", - "query-timeout", - "region", - "release-channel", - "remote-label", - "remove-domains", - "role", - "runtime", - "size", - "source", - "status", - "sub", - ...GEN_TYPES_LANGUAGE_VALUE_FLAG_NAMES, - "tail", - "template", - "timestamp", - "to", - "token", - "valid-for", - "version", - // Declared through a name-parameterized helper, invisible to the static scan (see the doc - // comment above): `issue.command.ts`'s `issueOptionalTextFlag`. (The `stringSliceFlag`-built - // names — domains, add-domains, remove-domains, config, exclude, override-name, db-unban-ip, - // db-allow-cidr — are already listed in the sections above.) - "additional-context", - "area", - "command", - "actual-output", - "expected-behavior", - "reproduce", - "crash-report-id", - "docker-services", - "problem", - "proposed-solution", - "alternatives", - "link", - "issue-type", - "improvement", - // experimental stack flags - "capability", - "service", - "since", - "stack", - "stack-id", - "preparation", -]); - -/** - * Short flags (without `-` prefix) that consume the next token as their value. - * Only single-character short flags need to be listed here. - */ -export const VALUE_CONSUMING_SHORT_FLAGS = new Set([ - "s", // --schema / -s - "o", // --output / -o - "p", // --password / -p (migration list, db push/pull/dump/remote) - "j", // --jobs / -j (storage cp) - "f", // --file / -f (db dump/diff/query, db schema declarative sync) - "t", // --template / -t (test new); --type / -t (sso add); --timestamp / -t (backups restore) - "x", // --exclude / -x (start, db dump) -]); - -/** - * Detects which of `--linked` / `--local` were explicitly set on the command line, for `seed - * buckets` and `storage ls/cp/mv/rm`'s mutually-exclusive-flag error message. - * - * Effect CLI's parsed flags carry no "was this set" bit, so this re-derives it from raw argv, - * skipping value tokens of space-separated value-consuming flags to avoid false positives. The - * negation form (`--no-linked`/`--no-local`) counts as changed. Returned alphabetically sorted - * (`["linked", "local"]`) so the rendered conflict string matches exactly. - */ -export function changedLinkedLocalFlags(args: ReadonlyArray): ReadonlyArray { - let linked = false; - let local = false; - let skipNext = false; - - for (const token of args) { - if (skipNext) { - skipNext = false; - continue; - } - if (token === "--") break; - - if (token.startsWith("--")) { - const eqIdx = token.indexOf("="); - const name = eqIdx === -1 ? token.slice(2) : token.slice(2, eqIdx); - const isBare = eqIdx === -1; - if (name === "linked" || name === "no-linked") { - linked = true; - continue; - } - if (name === "local" || name === "no-local") { - local = true; - continue; - } - if (isBare && VALUE_CONSUMING_LONG_FLAGS.has(name)) skipNext = true; - continue; - } - - if (token.startsWith("-") && token.length >= 2 && token.charAt(1) !== "-") { - if (token.length === 2 && VALUE_CONSUMING_SHORT_FLAGS.has(token.charAt(1))) { - skipNext = true; - } - } - } - - const setFlags: Array = []; - if (linked) setFlags.push("linked"); - if (local) setFlags.push("local"); - return setFlags; -} - -/** - * Resolves the DB target selection from raw CLI args with a single left-to-right pass, skipping - * value tokens that follow space-separated value-consuming flags to avoid false-positive - * detection. - * - * `setFlags` is built in the fixed alphabetical order `["db-url", "linked", "local"]` so the - * rendered conflict string (`[db-url linked]`, `[linked local]`, …) matches exactly. - * - * `connType` follows Changed-first precedence: - * 1. `--db-url` if changed → "db-url" - * 2. `--local` if changed → "local" - * 3. `--linked` if changed → "linked" - * 4. none changed → `undefined` (callers default to "local") - */ -export function resolveDbTargetFlags(args: ReadonlyArray): DbTargetSelection { - let dbUrlChanged = false; - let linkedChanged = false; - let localChanged = false; - - let skipNext = false; - for (const token of args) { - // A value-consuming flag consumes the next token as its value even when that token is "--". - // Only a "--" that is not a pending value acts as the end-of-options sentinel. - if (skipNext) { - skipNext = false; - continue; - } - - if (token === "--") break; - - if (token.startsWith("--")) { - const eqIdx = token.indexOf("="); - const name = eqIdx === -1 ? token.slice(2) : token.slice(2, eqIdx); - const isBare = eqIdx === -1; - - // Check target selectors. - if (name === "db-url") { - dbUrlChanged = true; - // --db-url is a string flag: in space form the next token is the value. - if (isBare) skipNext = true; - continue; - } - if (name === "linked" || name === "no-linked") { - linkedChanged = true; - continue; - } - if (name === "local" || name === "no-local") { - localChanged = true; - continue; - } - - // Non-target long flag: skip its value token if value-consuming and bare. - if (isBare && VALUE_CONSUMING_LONG_FLAGS.has(name)) { - skipNext = true; - } - continue; - } - - // Short flags: `-s`, `-o`, etc. - if (token.startsWith("-") && token.length >= 2 && token.charAt(1) !== "-") { - const shortName = token.charAt(1); - // `-s` bare (length === 2): next token is the value. - // `-svalue` (length > 2): value is attached, no skip needed. - if (token.length === 2 && VALUE_CONSUMING_SHORT_FLAGS.has(shortName)) { - skipNext = true; - } - } - } - - const setFlags: Array = []; - if (dbUrlChanged) setFlags.push("db-url"); - if (linkedChanged) setFlags.push("linked"); - if (localChanged) setFlags.push("local"); - - let connType: DbConnType | undefined; - if (dbUrlChanged) { - connType = "db-url"; - } else if (localChanged) { - connType = "local"; - } else if (linkedChanged) { - connType = "linked"; - } - - return { setFlags, connType }; -} diff --git a/apps/cli/src/command-internal/db-target-flags.unit.test.ts b/apps/cli/src/command-internal/db-target-flags.unit.test.ts deleted file mode 100644 index 45d1013257..0000000000 --- a/apps/cli/src/command-internal/db-target-flags.unit.test.ts +++ /dev/null @@ -1,336 +0,0 @@ -import { BunServices } from "@effect/platform-bun"; -import { describe, expect, it } from "@effect/vitest"; -import { Effect, FileSystem, Path } from "effect"; -import { fileURLToPath } from "node:url"; -import { - changedLinkedLocalFlags, - resolveDbTargetFlags, - VALUE_CONSUMING_LONG_FLAGS, - VALUE_CONSUMING_SHORT_FLAGS, -} from "./db-target-flags.ts"; - -describe("resolveDbTargetFlags", () => { - it("returns empty setFlags and undefined connType when no args", () => { - const result = resolveDbTargetFlags([]); - expect(result.setFlags).toEqual([]); - expect(result.connType).toBeUndefined(); - }); - - it("detects --linked as changed (connType='linked')", () => { - const result = resolveDbTargetFlags(["--linked"]); - expect(result.connType).toBe("linked"); - expect(result.setFlags).toEqual(["linked"]); - }); - - it("detects --linked=false as changed (Changed, not value)", () => { - const result = resolveDbTargetFlags(["db", "lint", "--linked=false"]); - expect(result.connType).toBe("linked"); - expect(result.setFlags).toEqual(["linked"]); - }); - - it("detects --no-linked as changed (boolean negation is still Changed)", () => { - const result = resolveDbTargetFlags(["--no-linked"]); - expect(result.connType).toBe("linked"); - expect(result.setFlags).toEqual(["linked"]); - }); - - it("detects --db-url as changed", () => { - const result = resolveDbTargetFlags(["--db-url", "postgres://x"]); - expect(result.connType).toBe("db-url"); - expect(result.setFlags).toEqual(["db-url"]); - }); - - it("detects --db-url= as changed", () => { - const result = resolveDbTargetFlags(["--db-url=postgres://x"]); - expect(result.connType).toBe("db-url"); - expect(result.setFlags).toEqual(["db-url"]); - }); - - it("--local=false --linked produces setFlags length 2 with alphabetical order [linked local]", () => { - const result = resolveDbTargetFlags(["--local=false", "--linked"]); - expect(result.setFlags).toEqual(["linked", "local"]); - expect(result.setFlags).toHaveLength(2); - expect(result.connType).toBe("local"); - }); - - it("--db-url=postgres://x --linked produces setFlags [db-url linked] with connType=db-url", () => { - const result = resolveDbTargetFlags(["--db-url=postgres://x", "--linked"]); - expect(result.setFlags).toEqual(["db-url", "linked"]); - expect(result.connType).toBe("db-url"); - }); - - it("tokens after bare -- are not scanned (end-of-options sentinel)", () => { - const result = resolveDbTargetFlags(["--", "--linked"]); - expect(result.setFlags).toEqual([]); - expect(result.connType).toBeUndefined(); - }); - - it("--db-url (key only, value as next arg) is still detected as changed", () => { - const result = resolveDbTargetFlags(["--db-url", "postgres://x"]); - expect(result.connType).toBe("db-url"); - expect(result.setFlags).toEqual(["db-url"]); - }); - - it("setFlags order is always alphabetical [db-url, linked, local] regardless of argv order", () => { - const result = resolveDbTargetFlags(["--local", "--db-url=x", "--linked"]); - expect(result.setFlags).toEqual(["db-url", "linked", "local"]); - }); - - it("Changed-first precedence: db-url > local > linked", () => { - const all = resolveDbTargetFlags(["--db-url=x", "--linked", "--local"]); - expect(all.connType).toBe("db-url"); - - const localLinked = resolveDbTargetFlags(["--linked", "--local"]); - expect(localLinked.connType).toBe("local"); - - const linkedOnly = resolveDbTargetFlags(["--linked"]); - expect(linkedOnly.connType).toBe("linked"); - }); - - it("skips value token after bare --schema so --linked is not a false positive", () => { - const result = resolveDbTargetFlags(["db", "lint", "--schema", "--linked"]); - expect(result.connType).toBeUndefined(); - expect(result.setFlags).toEqual([]); - }); - - it("skips value token after bare --level so following flags are not false positives", () => { - const result = resolveDbTargetFlags(["--level", "error", "--local"]); - expect(result.connType).toBe("local"); - expect(result.setFlags).toEqual(["local"]); - }); - - it("--schema=value (attached form) does NOT skip the next token", () => { - const result = resolveDbTargetFlags(["--schema=public", "--linked"]); - expect(result.connType).toBe("linked"); - expect(result.setFlags).toEqual(["linked"]); - }); - - it("skips value token after bare -s (short for --schema)", () => { - const result = resolveDbTargetFlags(["-s", "--linked"]); - expect(result.connType).toBeUndefined(); - expect(result.setFlags).toEqual([]); - }); - - it("-svalue (attached short form) does NOT skip the next token", () => { - const result = resolveDbTargetFlags(["-spublic", "--linked"]); - expect(result.connType).toBe("linked"); - expect(result.setFlags).toEqual(["linked"]); - }); - - it("skips value token after bare --output so following flags are not false positives", () => { - const result = resolveDbTargetFlags(["--output", "json", "--local"]); - expect(result.connType).toBe("local"); - expect(result.setFlags).toEqual(["local"]); - }); - - it("--output-dir does NOT mark --local as changed (value consumed)", () => { - const result = resolveDbTargetFlags(["--output-dir", "--local"]); - expect(result.connType).toBeUndefined(); - expect(result.setFlags).toEqual([]); - }); - - it("--output-dir= (attached form) DOES mark --local as changed", () => { - const result = resolveDbTargetFlags(["--output-dir=./reports", "--local"]); - expect(result.connType).toBe("local"); - expect(result.setFlags).toEqual(["local"]); - }); - - it("--schema -- --linked: -- consumed as schema value, --linked is a real flag (Go pflag parity)", () => { - const result = resolveDbTargetFlags(["db", "lint", "--schema", "--", "--linked"]); - expect(result.connType).toBe("linked"); - expect(result.setFlags).toEqual(["linked"]); - }); - - it("bare -- with no pending skip still stops the scan", () => { - const result = resolveDbTargetFlags(["--linked", "--", "--local"]); - expect(result.connType).toBe("linked"); - expect(result.setFlags).toEqual(["linked"]); - }); - - it("skips value token after bare -p so --local is the password value, not a target", () => { - const result = resolveDbTargetFlags(["migration", "list", "-p", "--local"]); - expect(result.connType).toBeUndefined(); - expect(result.setFlags).toEqual([]); - }); - - it("skips value token after bare --password so --linked is its value, not a flag", () => { - const result = resolveDbTargetFlags(["--password", "--linked"]); - expect(result.connType).toBeUndefined(); - expect(result.setFlags).toEqual([]); - }); - - it("-ppwd (attached short password) does NOT consume the next token", () => { - const result = resolveDbTargetFlags(["-ppwd", "--local"]); - expect(result.connType).toBe("local"); - expect(result.setFlags).toEqual(["local"]); - }); - - it("--password=pwd (attached long form) does NOT consume the next token", () => { - const result = resolveDbTargetFlags(["--password=pwd", "--local"]); - expect(result.connType).toBe("local"); - expect(result.setFlags).toEqual(["local"]); - }); -}); - -describe("VALUE_CONSUMING_LONG_FLAGS / VALUE_CONSUMING_SHORT_FLAGS completeness (CLI-1896 review)", () => { - // `extractChangedFlagNames` relies on these two sets across every command, not just the - // db-target subset this file's other describe block covers — see the doc comment on - // `VALUE_CONSUMING_LONG_FLAGS` in `db-target-flags.ts`. This scan is static-source-based - // rather than importing every command module, so it can only see flag names declared as a - // literal string; a name passed through a helper function is registered by hand instead. - const commandsDir = fileURLToPath(new URL("../commands", import.meta.url)); - const INDIRECT_NAME_FILES = new Set(["issue.command.ts"]); - const VALUE_FLAG_KINDS = ["string", "integer", "choice", "choiceWithValue", "float"]; - - const walk = Effect.fnUntraced(function* (dir: string) { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const entries = yield* fs.readDirectory(dir, { recursive: true }); - return entries - .filter((entry) => entry.endsWith(".command.ts")) - .map((entry) => path.join(dir, entry)); - }); - - interface DeclaredFlag { - readonly file: string; - readonly name: string; - readonly alias: string | undefined; - } - - function extractDeclaredFlags(filePath: string, source: string): Array { - const callRegex = /Flag\.(string|integer|choice|choiceWithValue|float|boolean)\(/g; - const calls = Array.from(source.matchAll(callRegex), (match) => ({ - index: match.index, - kind: match[1]!, - })); - - const declared: Array = []; - for (let i = 0; i < calls.length; i++) { - const current = calls[i]!; - if (!VALUE_FLAG_KINDS.includes(current.kind)) continue; - - // Name declared as a literal string (e.g. `Flag.String("schema")`); a name passed as an - // identifier doesn't match and is silently skipped — see INDIRECT_NAME_FILES above. - const remainder = source.slice(current.index); - const nameMatch = remainder.match(/^Flag\.\w+\(\s*"([a-zA-Z0-9-]+)"/); - if (!nameMatch) continue; - - // The alias, if any, is somewhere in the `.pipe(...)` chain between - // this flag declaration and the next one. - const windowEnd = i + 1 < calls.length ? calls[i + 1]!.index : source.length; - const window = source.slice(current.index, windowEnd); - const aliasMatch = window.match(/withAlias\(\s*"([a-zA-Z0-9])"\s*\)/); - - declared.push({ file: filePath, name: nameMatch[1]!, alias: aliasMatch?.[1] }); - } - return declared; - } - - it.effect( - "registers every directly-declared value-consuming flag name in VALUE_CONSUMING_LONG_FLAGS", - () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const missing: Array = []; - - for (const filePath of yield* walk(commandsDir)) { - if (INDIRECT_NAME_FILES.has(path.basename(filePath))) continue; - - for (const flag of extractDeclaredFlags(filePath, yield* fs.readFileString(filePath))) { - if (!VALUE_CONSUMING_LONG_FLAGS.has(flag.name)) { - missing.push(`${flag.name} (${path.relative(commandsDir, flag.file)})`); - } - } - } - - expect(missing).toEqual([]); - }).pipe(Effect.provide(BunServices.layer)), - ); - - it.effect( - "registers every directly-declared value-consuming flag's shorthand in VALUE_CONSUMING_SHORT_FLAGS", - () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const missing: Array = []; - - for (const filePath of yield* walk(commandsDir)) { - if (INDIRECT_NAME_FILES.has(path.basename(filePath))) continue; - - for (const flag of extractDeclaredFlags(filePath, yield* fs.readFileString(filePath))) { - if (flag.alias !== undefined && !VALUE_CONSUMING_SHORT_FLAGS.has(flag.alias)) { - missing.push( - `-${flag.alias} (--${flag.name}, ${path.relative(commandsDir, flag.file)})`, - ); - } - } - } - - expect(missing).toEqual([]); - }).pipe(Effect.provide(BunServices.layer)), - ); -}); - -describe("changedLinkedLocalFlags", () => { - it("returns nothing when neither selector is present", () => { - expect(changedLinkedLocalFlags(["seed", "buckets"])).toEqual([]); - expect(changedLinkedLocalFlags(["storage", "ls", "ss:///"])).toEqual([]); - }); - - it("returns a single selector", () => { - expect(changedLinkedLocalFlags(["seed", "buckets", "--linked"])).toEqual(["linked"]); - expect(changedLinkedLocalFlags(["seed", "buckets", "--local"])).toEqual(["local"]); - }); - - it("returns both selectors in cobra's sorted order when both are set", () => { - expect(changedLinkedLocalFlags(["seed", "buckets", "--local", "--linked"])).toEqual([ - "linked", - "local", - ]); - }); - - it("handles = forms", () => { - expect(changedLinkedLocalFlags(["--local=true", "--linked=false"])).toEqual([ - "linked", - "local", - ]); - }); - - it("treats the --no-* negation form as changed", () => { - expect(changedLinkedLocalFlags(["seed", "buckets", "--no-linked"])).toEqual(["linked"]); - expect(changedLinkedLocalFlags(["storage", "ls", "--no-local"])).toEqual(["local"]); - expect(changedLinkedLocalFlags(["seed", "buckets", "--no-local", "--linked"])).toEqual([ - "linked", - "local", - ]); - }); - - it("does not treat a value-consuming flag's value as a selector", () => { - expect(changedLinkedLocalFlags(["seed", "buckets", "--workdir", "--linked"])).toEqual([]); - expect(changedLinkedLocalFlags(["-o", "--linked", "--local"])).toEqual(["local"]); - expect( - changedLinkedLocalFlags(["storage", "cp", "--content-type", "--local", "a", "b"]), - ).toEqual([]); - expect( - changedLinkedLocalFlags(["storage", "cp", "--cache-control", "--linked", "a", "b"]), - ).toEqual([]); - expect(changedLinkedLocalFlags(["storage", "cp", "--jobs", "--local", "a", "b"])).toEqual([]); - expect(changedLinkedLocalFlags(["storage", "cp", "-j", "--linked", "a", "b"])).toEqual([]); - }); - - it("still detects a selector after a value-consuming flag's value", () => { - expect(changedLinkedLocalFlags(["storage", "cp", "--jobs", "5", "--local", "a", "b"])).toEqual([ - "local", - ]); - }); - - it("stops scanning at the -- terminator", () => { - expect(changedLinkedLocalFlags(["seed", "buckets", "--", "--local", "--linked"])).toEqual([]); - }); - - it("detects selectors given after positional arguments", () => { - expect(changedLinkedLocalFlags(["storage", "rm", "ss:///b/x", "--local"])).toEqual(["local"]); - }); -}); diff --git a/apps/cli/src/command-internal/debug-logger.layer.ts b/apps/cli/src/command-internal/debug-logger.layer.ts index b0a1c75d43..a2de6fc90c 100644 --- a/apps/cli/src/command-internal/debug-logger.layer.ts +++ b/apps/cli/src/command-internal/debug-logger.layer.ts @@ -1,11 +1,11 @@ import { DateTime, Effect, Layer } from "effect"; import { DebugFlag } from "./global-flags.ts"; -import { DebugLogger } from "./debug-logger.service.ts"; +import { DebugLogger } from "../shared/output/debug-logger.service.ts"; const pad = (n: number): string => String(n).padStart(2, "0"); -/** Formats a timestamp matching Go's `log.LstdFlags`: `YYYY/MM/DD HH:MM:SS`. */ +/** Formats a timestamp as `YYYY/MM/DD HH:MM:SS`. */ function formatTimestamp(now: DateTime.DateTime): string { const local = DateTime.toParts(DateTime.setZone(now, DateTime.zoneMakeLocal())); return ( diff --git a/apps/cli/src/command-internal/debug-logger.layer.unit.test.ts b/apps/cli/src/command-internal/debug-logger.layer.unit.test.ts index 846ae8b799..98592d656c 100644 --- a/apps/cli/src/command-internal/debug-logger.layer.unit.test.ts +++ b/apps/cli/src/command-internal/debug-logger.layer.unit.test.ts @@ -6,7 +6,7 @@ import { vi } from "vitest"; import { withEnvVar } from "../../tests/helpers/command-mocks.ts"; import { DebugFlag } from "./global-flags.ts"; import { debugLoggerLayer } from "./debug-logger.layer.ts"; -import { DebugLogger } from "./debug-logger.service.ts"; +import { DebugLogger } from "../shared/output/debug-logger.service.ts"; function makeLayer(debug: boolean) { return debugLoggerLayer.pipe(Layer.provide(Layer.succeed(DebugFlag, debug))); @@ -44,7 +44,7 @@ describe("debugLoggerLayer", () => { ); }); - it.effect("http emits Go timestamp order and method/url format", () => { + it.effect("http emits timestamp, method and url in order", () => { const stderr = captureStderr(); const body = Effect.gen(function* () { const localTime = DateTime.makeZonedUnsafe( diff --git a/apps/cli/src/command-internal/detect-content-type.ts b/apps/cli/src/command-internal/detect-content-type.ts index 2c01986594..f6550503eb 100644 --- a/apps/cli/src/command-internal/detect-content-type.ts +++ b/apps/cli/src/command-internal/detect-content-type.ts @@ -1,6 +1,6 @@ /** * Content-type sniffer for the `seed buckets` upload path: Storage's `Content-Type` metadata is - * byte-driven, not extension-driven, matching Go's `net/http.DetectContentType` byte-for-byte so + * byte-driven, not extension-driven, so * stored metadata stays reproducible. Signature order matters: first match wins. */ diff --git a/apps/cli/src/command-internal/detect-content-type.unit.test.ts b/apps/cli/src/command-internal/detect-content-type.unit.test.ts index 70fc8d0483..8c93db206f 100644 --- a/apps/cli/src/command-internal/detect-content-type.unit.test.ts +++ b/apps/cli/src/command-internal/detect-content-type.unit.test.ts @@ -10,8 +10,6 @@ function bytes(s: string): Uint8Array { } describe("detectContentType", () => { - // Expected values were produced by running Go's `http.DetectContentType` over these same - // byte inputs. const corpus: ReadonlyArray = [ ["png", "\x89PNG\x0D\x0A\x1A\x0A\x00\x00", "image/png"], ["pdf", "%PDF-1.4\n...", "application/pdf"], @@ -37,7 +35,7 @@ describe("detectContentType", () => { ]; for (const [name, input, expected] of corpus) { - it(`matches Go http.DetectContentType for ${name}`, () => { + it(`detects the expected content type for ${name}`, () => { expect(detectContentType(bytes(input))).toBe(expected); }); } diff --git a/apps/cli/src/command-internal/diff-engine.ts b/apps/cli/src/command-internal/diff-engine.ts index e0ebc1b3a5..b6ff5b8430 100644 --- a/apps/cli/src/command-internal/diff-engine.ts +++ b/apps/cli/src/command-internal/diff-engine.ts @@ -2,34 +2,18 @@ // dependencies — unit-tested directly. export const schemaPathsTransitionWarning = - "WARNING: [db.migrations].schema_paths no longer changes the migrations baseline used by db diff or migration-style db pull. These commands always compare local migrations with the selected database. Use `supabase db schema declarative sync` to compare declarative schema files.\n"; + "WARNING: [db.migrations].schema_paths no longer changes the migrations baseline used by db diff or migration-style db pull. These commands always compare local migrations with the selected database. `supabase db schema declarative sync` reads declarative_schema_path (default supabase/schemas), not schema_paths: move those files there or point declarative_schema_path at them. Only migra (--use-migra, --diff-engine migra, or [experimental.pgdelta] enabled = false) still reads schema_paths, and only for local targets.\n"; /** - * Whether pg-delta is the active default engine: the resolved config flag - * (`[experimental.pgdelta].enabled`), the command's `--use-pg-delta` flag, or the - * `SUPABASE_EXPERIMENTAL_PG_DELTA` env var, whichever is set. - */ -export function shouldUsePgDelta(inputs: { - readonly configEnabled: boolean; - readonly usePgDeltaFlag: boolean; - readonly envEnabled: boolean; -}): boolean { - return inputs.configEnabled || inputs.usePgDeltaFlag || inputs.envEnabled; -} - -/** - * Reports whether `db diff` should run in pg-delta mode. An explicit `--use-migra`, - * `--use-pgadmin`, or `--use-pg-schema` is an authoritative rollback that clears pg-delta mode; - * `--use-migra` defaults to true, so only an explicit pass (`useMigraChanged`) counts as opting - * out. + * Reports whether `db diff` should run in pg-delta mode. A true `--use-migra` or `--use-pgadmin` + * is an authoritative rollback that clears pg-delta mode; `--use-migra=false` keeps the default. */ export function resolveDiffEngine(inputs: { - readonly useMigraChanged: boolean; + readonly useMigra: boolean; readonly usePgAdmin: boolean; - readonly usePgSchema: boolean; readonly pgDeltaDefault: boolean; }): boolean { - if (inputs.useMigraChanged || inputs.usePgAdmin || inputs.usePgSchema) { + if (inputs.useMigra || inputs.usePgAdmin) { return false; } return inputs.pgDeltaDefault; @@ -50,40 +34,3 @@ export function resolvePullDiffEngine(inputs: { } return inputs.pgDeltaDefault; } - -/** - * Parses a boolean env var: accepts exactly `1`, `t`, `T`, `TRUE`, `true`, `True` as true, and - * every other value (including unparseable strings and unset) as false. - */ -export function parseBoolEnv(raw: string | undefined): boolean { - switch (raw) { - case "1": - case "t": - case "T": - case "TRUE": - case "true": - case "True": - return true; - default: - return false; - } -} - -/** - * Resolves `db pull` declarative mode from raw argv: `--declarative` and the deprecated - * `--use-pg-delta` alias bind to the same outcome, so the last occurrence in argv wins (ORing - * the two would wrongly select declarative mode from either flag alone). A bare flag is `true`; - * `--flag=value` parses with {@link parseBoolEnv}'s true-set — a following space-separated token - * is never consumed as a value. Returns `undefined` when neither flag is present. - */ -export function resolveDeclarativeFromArgs(args: ReadonlyArray): boolean | undefined { - const FLAG_PATTERN = /^--(?:declarative|use-pg-delta)(?:=(.*))?$/u; - let result: boolean | undefined; - for (const arg of args) { - if (arg === "--") break; - const match = FLAG_PATTERN.exec(arg); - if (match === null) continue; - result = match[1] === undefined ? true : parseBoolEnv(match[1]); - } - return result; -} diff --git a/apps/cli/src/command-internal/diff-engine.unit.test.ts b/apps/cli/src/command-internal/diff-engine.unit.test.ts index 7f1a7998b9..6286e19af8 100644 --- a/apps/cli/src/command-internal/diff-engine.unit.test.ts +++ b/apps/cli/src/command-internal/diff-engine.unit.test.ts @@ -1,35 +1,11 @@ import { describe, expect, it } from "vitest"; -import { - parseBoolEnv, - resolveDeclarativeFromArgs, - resolveDiffEngine, - resolvePullDiffEngine, - shouldUsePgDelta, -} from "./diff-engine.ts"; - -describe("shouldUsePgDelta", () => { - it("is the OR of config, flag, and env", () => { - expect( - shouldUsePgDelta({ configEnabled: false, usePgDeltaFlag: false, envEnabled: false }), - ).toBe(false); - expect( - shouldUsePgDelta({ configEnabled: true, usePgDeltaFlag: false, envEnabled: false }), - ).toBe(true); - expect( - shouldUsePgDelta({ configEnabled: false, usePgDeltaFlag: true, envEnabled: false }), - ).toBe(true); - expect( - shouldUsePgDelta({ configEnabled: false, usePgDeltaFlag: false, envEnabled: true }), - ).toBe(true); - }); -}); +import { resolveDiffEngine, resolvePullDiffEngine } from "./diff-engine.ts"; describe("resolveDiffEngine", () => { const base = { - useMigraChanged: false, + useMigra: false, usePgAdmin: false, - usePgSchema: false, pgDeltaDefault: true, }; @@ -39,16 +15,12 @@ describe("resolveDiffEngine", () => { }); it("an explicit --use-migra clears pg-delta mode", () => { - expect(resolveDiffEngine({ ...base, useMigraChanged: true })).toBe(false); + expect(resolveDiffEngine({ ...base, useMigra: true })).toBe(false); }); it("--use-pgadmin clears pg-delta mode", () => { expect(resolveDiffEngine({ ...base, usePgAdmin: true })).toBe(false); }); - - it("--use-pg-schema clears pg-delta mode", () => { - expect(resolveDiffEngine({ ...base, usePgSchema: true })).toBe(false); - }); }); describe("resolvePullDiffEngine", () => { @@ -86,49 +58,3 @@ describe("resolvePullDiffEngine", () => { ).toBe(false); }); }); - -describe("parseBoolEnv", () => { - it("accepts only strconv.ParseBool truthy strings", () => { - for (const v of ["1", "t", "T", "TRUE", "true", "True"]) { - expect(parseBoolEnv(v)).toBe(true); - } - }); - - it("treats every other value (including unset) as false", () => { - for (const v of ["0", "f", "FALSE", "false", "yes", "on", "2", "", "TrUe"]) { - expect(parseBoolEnv(v)).toBe(false); - } - expect(parseBoolEnv(undefined)).toBe(false); - }); -}); - -describe("resolveDeclarativeFromArgs", () => { - it("returns undefined when neither flag is present", () => { - expect(resolveDeclarativeFromArgs(["db", "pull"])).toBeUndefined(); - expect(resolveDeclarativeFromArgs([])).toBeUndefined(); - }); - - it("treats a bare flag as true", () => { - expect(resolveDeclarativeFromArgs(["db", "pull", "--declarative"])).toBe(true); - expect(resolveDeclarativeFromArgs(["db", "pull", "--use-pg-delta"])).toBe(true); - }); - - it("parses an =value with strconv.ParseBool semantics", () => { - expect(resolveDeclarativeFromArgs(["--declarative=false"])).toBe(false); - expect(resolveDeclarativeFromArgs(["--declarative=true"])).toBe(true); - expect(resolveDeclarativeFromArgs(["--use-pg-delta=0"])).toBe(false); - expect(resolveDeclarativeFromArgs(["--use-pg-delta=1"])).toBe(true); - }); - - it("lets the last occurrence win across both flag names (pflag single-variable bind)", () => { - expect(resolveDeclarativeFromArgs(["--declarative", "--use-pg-delta=false"])).toBe(false); - expect(resolveDeclarativeFromArgs(["--use-pg-delta", "--declarative=false"])).toBe(false); - expect(resolveDeclarativeFromArgs(["--declarative=false", "--use-pg-delta"])).toBe(true); - expect(resolveDeclarativeFromArgs(["--use-pg-delta=false", "--declarative"])).toBe(true); - }); - - it("ignores tokens after the `--` argv terminator", () => { - expect(resolveDeclarativeFromArgs(["--declarative", "--", "--use-pg-delta=false"])).toBe(true); - expect(resolveDeclarativeFromArgs(["--", "--declarative"])).toBeUndefined(); - }); -}); diff --git a/apps/cli/src/command-internal/docker-ids.ts b/apps/cli/src/command-internal/docker-ids.ts index 7be84f785e..389576db84 100644 --- a/apps/cli/src/command-internal/docker-ids.ts +++ b/apps/cli/src/command-internal/docker-ids.ts @@ -4,46 +4,7 @@ * the local stack is running. */ -import type { Path } from "effect"; - -/** - * Resolves the local project id used to derive Docker resource names. - * - * Precedence: `SUPABASE_PROJECT_ID` env var, then config.toml's `project_id`, then - * `--project-ref` (when the command accepts one), then the working directory's basename. - */ -export function resolveLocalProjectId( - path: Path.Path, - envProjectId: string | undefined, - tomlProjectId: string | undefined, - workdir: string, - projectRefDefault?: string, -): string { - if (envProjectId !== undefined && envProjectId.length > 0) return envProjectId; - if (tomlProjectId !== undefined && tomlProjectId.length > 0) return tomlProjectId; - if (projectRefDefault !== undefined && projectRefDefault.length > 0) return projectRefDefault; - return path.basename(workdir); -} - -const INVALID_PROJECT_ID = /[^a-zA-Z0-9_.-]+/g; -const MAX_PROJECT_ID_LENGTH = 40; - -function truncateText(text: string, maxLength: number) { - return text.length > maxLength ? text.slice(0, maxLength) : text; -} - -/** - * Sanitizes a project id for use in container/network names: replaces invalid character runs - * with `_`, strips leading `_.-`, and caps the result at 40 characters. - * - * Exported separately from {@link serviceContainerName} because a Docker label filter built - * from a project id must be sanitized the same way, or a `project_id` like `"my app"` won't - * match the label `start` wrote (see {@link cliProjectFilterValue}). - */ -export function sanitizeProjectId(src: string) { - const sanitized = src.replaceAll(INVALID_PROJECT_ID, "_").replace(/^[_.-]+/, ""); - return truncateText(sanitized, MAX_PROJECT_ID_LENGTH); -} +import { sanitizeProjectId } from "../shared/config/project-id.ts"; /** `supabase__` — the naming scheme for local Docker resources. */ export function serviceContainerName(suffix: string, projectId: string): string { diff --git a/apps/cli/src/command-internal/docker-ids.unit.test.ts b/apps/cli/src/command-internal/docker-ids.unit.test.ts index cbea70ac5e..f4c6a2085e 100644 --- a/apps/cli/src/command-internal/docker-ids.unit.test.ts +++ b/apps/cli/src/command-internal/docker-ids.unit.test.ts @@ -1,54 +1,20 @@ -import { BunPath } from "@effect/platform-bun"; -import { describe, expect, it, layer } from "@effect/vitest"; -import { ConfigProvider, Effect, Path } from "effect"; +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; + +import { withConfigEnv } from "../../tests/helpers/command-mocks.ts"; import { CLI_PROJECT_LABEL, cliProjectFilterValue, - resolveLocalProjectId, - sanitizeProjectId, serviceContainerIds, - localDbContainerId, localNetworkId, } from "./docker-ids.ts"; +import { sanitizeProjectId } from "../shared/config/project-id.ts"; import { resolveDockerNetworkMode } from "../shared/functions/functions-docker.ts"; -import { viperEnvStringWithProjectFallback } from "./viper-env.ts"; - -layer(BunPath.layer)("resolveLocalProjectId", (it) => { - it.effect("prefers SUPABASE_PROJECT_ID (env) over config.toml and the basename", () => - Effect.gen(function* () { - const path = yield* Path.Path; - expect(resolveLocalProjectId(path, "env-id", "toml-id", "/work/proj")).toBe("env-id"); - }), - ); - - it.effect("falls back to config.toml project_id when the env var is unset/empty", () => - Effect.gen(function* () { - const path = yield* Path.Path; - expect(resolveLocalProjectId(path, undefined, "toml-id", "/work/proj")).toBe("toml-id"); - expect(resolveLocalProjectId(path, "", "toml-id", "/work/proj")).toBe("toml-id"); - }), - ); - - it.effect("falls back to the workdir basename when both env and config.toml are absent", () => - Effect.gen(function* () { - const path = yield* Path.Path; - expect(resolveLocalProjectId(path, undefined, undefined, "/work/my-app")).toBe("my-app"); - expect(resolveLocalProjectId(path, undefined, "", "/work/my-app")).toBe("my-app"); - }), - ); - - it.effect("feeds the resolved id into the local db container name", () => - Effect.gen(function* () { - const path = yield* Path.Path; - const id = resolveLocalProjectId(path, "env-id", undefined, "/work/proj"); - expect(localDbContainerId(id)).toBe("supabase_db_env-id"); - }), - ); -}); +import { supabaseEnvStringWithProjectFallback } from "./supabase-env.ts"; describe("serviceContainerIds", () => { - it("returns the 13 service container ids in Go's GetDockerIds() order", () => { + it("returns the 13 service container ids in order", () => { expect(serviceContainerIds("my-app")).toEqual([ "supabase_kong_my-app", "supabase_auth_my-app", @@ -73,7 +39,7 @@ describe("serviceContainerIds", () => { }); describe("cliProjectFilterValue", () => { - it("returns the bare label when the project id is empty (Go's --all path)", () => { + it("returns the bare label when the project id is empty (the --all path)", () => { expect(cliProjectFilterValue("")).toBe(CLI_PROJECT_LABEL); }); @@ -88,94 +54,52 @@ describe("cliProjectFilterValue", () => { }); }); -describe("resolveDockerNetworkMode composed with viperEnvStringWithProjectFallback (start/db start call shape)", () => { +describe("resolveDockerNetworkMode composed with supabaseEnvStringWithProjectFallback (start/db start call shape)", () => { const KEY = "SUPABASE_NETWORK_ID"; - const withShellEnv = (env: Readonly>) => - Effect.provide( - ConfigProvider.layer(ConfigProvider.fromEnvRecord(env, { preserveEmptyStrings: true })), + function resolve( + flagValue: string | undefined, + projectEnv: Record, + shell?: string, + ) { + return Effect.runSync( + withConfigEnv( + shell === undefined ? {} : { [KEY]: shell }, + Effect.map(supabaseEnvStringWithProjectFallback(KEY, projectEnv), (envNetworkId) => + resolveDockerNetworkMode({ explicit: flagValue, envNetworkId, projectId: "my-app" }), + ), + ), ); - - function resolve(flagValue: string | undefined, projectEnv: Record) { - return Effect.gen(function* () { - return resolveDockerNetworkMode({ - explicit: flagValue, - envOverride: yield* viperEnvStringWithProjectFallback(KEY, projectEnv), - projectId: "my-app", - }); - }); } - it.effect("prefers an explicit --network-id flag over everything else", () => - Effect.gen(function* () { - expect( - yield* resolve("flag-network", { [KEY]: "toml-network" }).pipe( - withShellEnv({ [KEY]: "env-network" }), - ), - ).toBe("flag-network"); - }), - ); - - it.effect("falls back to SUPABASE_NETWORK_ID (shell) when the flag is absent", () => - Effect.gen(function* () { - expect(yield* resolve(undefined, {}).pipe(withShellEnv({ [KEY]: "shell-network" }))).toBe( - "shell-network", - ); - }), - ); - - it.effect( - "falls back to SUPABASE_NETWORK_ID (project .env) when both the flag and shell are absent", - () => - Effect.gen(function* () { - expect(yield* resolve(undefined, { [KEY]: "project-network" }).pipe(withShellEnv({}))).toBe( - "project-network", - ); - }), - ); - - it.effect( - "prefers the shell value over the project .env value (presence wins, matching godotenv.Load)", - () => - Effect.gen(function* () { - expect( - yield* resolve(undefined, { [KEY]: "project-network" }).pipe( - withShellEnv({ [KEY]: "shell-network" }), - ), - ).toBe("shell-network"); - }), - ); - - it.effect( - "falls back to the generated network name when the flag and env are all absent/empty", - () => - Effect.gen(function* () { - expect(yield* resolve(undefined, {}).pipe(withShellEnv({}))).toBe(localNetworkId("my-app")); - expect(yield* resolve("", {}).pipe(withShellEnv({}))).toBe(localNetworkId("my-app")); - }), - ); - - it.effect( - "an explicit-but-empty --network-id= skips the env var entirely (viper: a Changed pflag resolves before AutomaticEnv)", - () => - Effect.gen(function* () { - expect( - yield* resolve("", { [KEY]: "project-network" }).pipe( - withShellEnv({ [KEY]: "env-network" }), - ), - ).toBe(localNetworkId("my-app")); - }), - ); - - it.effect( - "treats an empty shell value as present (blocks the project value) and falls to generated", - () => - Effect.gen(function* () { - expect( - yield* resolve(undefined, { [KEY]: "project-network" }).pipe(withShellEnv({ [KEY]: "" })), - ).toBe(localNetworkId("my-app")); - }), - ); + it("prefers an explicit --network-id flag over everything else", () => { + expect(resolve("flag-network", { [KEY]: "toml-network" }, "env-network")).toBe("flag-network"); + }); + + it("falls back to SUPABASE_NETWORK_ID (shell) when the flag is absent", () => { + expect(resolve(undefined, {}, "shell-network")).toBe("shell-network"); + }); + + it("falls back to SUPABASE_NETWORK_ID (project .env) when both the flag and shell are absent", () => { + expect(resolve(undefined, { [KEY]: "project-network" })).toBe("project-network"); + }); + + it("prefers the shell value over the project .env value", () => { + expect(resolve(undefined, { [KEY]: "project-network" }, "shell-network")).toBe("shell-network"); + }); + + it("falls back to the generated network name when the flag and env are all absent/empty", () => { + expect(resolve(undefined, {})).toBe(localNetworkId("my-app")); + expect(resolve("", {})).toBe(localNetworkId("my-app")); + }); + + it("an explicit-but-empty --network-id= skips the env var entirely", () => { + expect(resolve("", { [KEY]: "project-network" }, "env-network")).toBe(localNetworkId("my-app")); + }); + + it("treats an empty shell value as present (blocks the project value) and falls to generated", () => { + expect(resolve(undefined, { [KEY]: "project-network" }, "")).toBe(localNetworkId("my-app")); + }); }); describe("sanitizeProjectId", () => { diff --git a/apps/cli/src/command-internal/docker-image-resolve.ts b/apps/cli/src/command-internal/docker-image-resolve.ts index cb846f3007..bf1006e7f4 100644 --- a/apps/cli/src/command-internal/docker-image-resolve.ts +++ b/apps/cli/src/command-internal/docker-image-resolve.ts @@ -130,14 +130,14 @@ export function makeDockerImageResolver( Stream.runForEach(handle.stdout, (chunk) => Effect.sync(() => { stdoutChunks.push(chunk); - globalThis.process.stderr.write(chunk); + process.stderr.write(chunk); if (chunk.length > 0) endedWithNewline = chunk[chunk.length - 1] === 0x0a; }), ), Stream.runForEach(handle.stderr, (chunk) => Effect.sync(() => { stderrChunks.push(chunk); - globalThis.process.stderr.write(chunk); + process.stderr.write(chunk); if (chunk.length > 0) endedWithNewline = chunk[chunk.length - 1] === 0x0a; }), ), @@ -250,9 +250,9 @@ export function makeDockerImageResolver( // one, so the two don't glue together. yield* Effect.sync(() => { if (!lastPullEndedWithNewline) { - globalThis.process.stderr.write("\n"); + process.stderr.write("\n"); } - globalThis.process.stderr.write(`Retrying after ${delay / 1000}s: ${candidate}\n`); + process.stderr.write(`Retrying after ${delay / 1000}s: ${candidate}\n`); }); yield* Effect.sleep(`${delay} millis`); } diff --git a/apps/cli/src/command-internal/docker-lifecycle.unit.test.ts b/apps/cli/src/command-internal/docker-lifecycle.unit.test.ts index 8c4b435d57..f6491f3792 100644 --- a/apps/cli/src/command-internal/docker-lifecycle.unit.test.ts +++ b/apps/cli/src/command-internal/docker-lifecycle.unit.test.ts @@ -294,19 +294,16 @@ describe("inspectContainerState", () => { ); }); - it.live( - "treats a paused/restarting container as running, matching Go's boolean-based gate", - () => { - const mock = mockSpawner({ - stdout: JSON.stringify({ Status: "paused", Running: true, ExitCode: 0 }), - }); - return inspectContainerState(mock.spawner, "supabase_db_my-app").pipe( - Effect.map((state) => { - expect(state).toEqual({ running: true, status: "paused", exitCode: 0, oomKilled: false }); - }), - ); - }, - ); + it.live("treats a paused/restarting container as running", () => { + const mock = mockSpawner({ + stdout: JSON.stringify({ Status: "paused", Running: true, ExitCode: 0 }), + }); + return inspectContainerState(mock.spawner, "supabase_db_my-app").pipe( + Effect.map((state) => { + expect(state).toEqual({ running: true, status: "paused", exitCode: 0, oomKilled: false }); + }), + ); + }); it.live( "fails with DockerLifecycleInspectError, preserving the real stderr, when the container does not exist", diff --git a/apps/cli/src/command-internal/docker-registry.unit.test.ts b/apps/cli/src/command-internal/docker-registry.unit.test.ts index 8489afdcbd..efe631df6d 100644 --- a/apps/cli/src/command-internal/docker-registry.unit.test.ts +++ b/apps/cli/src/command-internal/docker-registry.unit.test.ts @@ -81,7 +81,7 @@ describe("getRegistryImageUrl", () => { ]); }); - it("honors a projectEnvValues (dotenv)-only registry override, matching Go's post-Load os.Getenv", () => { + it("honors a projectEnvValues (dotenv)-only registry override", () => { expect( resolveImage(undefined, "supabase/pg_prove:3.36", { SUPABASE_INTERNAL_IMAGE_REGISTRY: "my.mirror.example", diff --git a/apps/cli/src/command-internal/docker-remove-all.ts b/apps/cli/src/command-internal/docker-remove-all.ts index e43a0b69b6..c4f81138a3 100644 --- a/apps/cli/src/command-internal/docker-remove-all.ts +++ b/apps/cli/src/command-internal/docker-remove-all.ts @@ -89,7 +89,7 @@ function parsePrunedNames(stdout: string): ReadonlyArray { const reportPruned = (debug: boolean, label: string, stdout: string) => Effect.sync(() => { if (!debug) return; - globalThis.process.stderr.write(`${label} [${parsePrunedNames(stdout).join(" ")}]\n`); + process.stderr.write(`${label} [${parsePrunedNames(stdout).join(" ")}]\n`); }); /** diff --git a/apps/cli/src/command-internal/docker-run.args.unit.test.ts b/apps/cli/src/command-internal/docker-run.args.unit.test.ts index f38831461b..f448a9bc43 100644 --- a/apps/cli/src/command-internal/docker-run.args.unit.test.ts +++ b/apps/cli/src/command-internal/docker-run.args.unit.test.ts @@ -26,7 +26,7 @@ describe("applyBitbucketDockerFilter", () => { expect(applyBitbucketDockerFilter(pgDelta, false)).toBe(pgDelta); }); - test("drops named-volume binds and clears security-opt under Bitbucket (Go DockerStart)", () => { + test("drops named-volume binds and clears security-opt under Bitbucket", () => { const filtered = applyBitbucketDockerFilter(pgDelta, true); // Named Deno-cache volume dropped; the /repo:/workspace bind mount kept. expect(filtered.binds).toEqual(["/repo:/workspace"]); @@ -35,7 +35,7 @@ describe("applyBitbucketDockerFilter", () => { }); describe("buildDockerArgs", () => { - test("assembles run args in Go-parity order for a named network", () => { + test("assembles run args in order for a named network", () => { expect(buildDockerArgs(base)).toEqual([ "run", "--rm", @@ -114,7 +114,7 @@ describe("buildDockerArgs", () => { expect(buildDockerArgs(base)).not.toContain("--label"); }); - test("emits --label k=v for each entry, before the image (Go's DockerStart project labels)", () => { + test("emits --label k=v for each entry, before the image", () => { const args = buildDockerArgs({ ...base, labels: { diff --git a/apps/cli/src/command-internal/docker-run.layer.ts b/apps/cli/src/command-internal/docker-run.layer.ts index 535238b569..fb3edc8f1c 100644 --- a/apps/cli/src/command-internal/docker-run.layer.ts +++ b/apps/cli/src/command-internal/docker-run.layer.ts @@ -89,7 +89,7 @@ export const dockerRunLayer: Layer.Layer { - it("detects the docker/podman daemon-down CLI messages (Go's IsErrConnectionFailed)", () => { + it("detects the docker/podman daemon-down CLI messages", () => { expect( isDockerDaemonUnreachable( "Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?", @@ -35,7 +35,7 @@ describe("isDockerDaemonUnreachable", () => { expect(isDockerDaemonUnreachable("")).toBe(false); }); - it("exposes Go's install hint verbatim", () => { + it("exposes the install hint verbatim", () => { expect(SUGGEST_DOCKER_INSTALL).toContain("https://docs.docker.com/desktop"); }); }); diff --git a/apps/cli/src/command-internal/go-duration.ts b/apps/cli/src/command-internal/duration.ts similarity index 91% rename from apps/cli/src/command-internal/go-duration.ts rename to apps/cli/src/command-internal/duration.ts index f9ab1f3526..0e11cd8fc7 100644 --- a/apps/cli/src/command-internal/go-duration.ts +++ b/apps/cli/src/command-internal/duration.ts @@ -1,6 +1,5 @@ /** - * Duration string parsing and formatting matching Go's `time.ParseDuration()`/`Duration.String()` - * grammar and rounding — the syntax the hosted API and container env vars expect. + * Duration string parsing and formatting in the `1h30m0s` grammar with nanosecond rounding — the syntax the hosted API and container env vars expect. * * Several `config.toml` duration fields (e.g. `auth.sessions.timebox = "1h"`) are stored as the * raw string, but a container's env var is built from the parsed-then-reserialized value, which @@ -15,7 +14,7 @@ const NS_PER_HOUR = 60 * NS_PER_MINUTE; const NS_PER_MS = 1_000_000; const NS_PER_US = 1_000; -// `BigInt` constants for `parseGoDuration`'s accumulator: near the int64 nanosecond ceiling +// `BigInt` constants for `parseDuration`'s accumulator: near the int64 nanosecond ceiling // (~9.223e18, past `Number.MAX_SAFE_INTEGER`), a `number` accumulator would silently round a // legitimate boundary value into a false overflow or vice versa. const NS_PER_SECOND_BIG = 1_000_000_000n; @@ -34,7 +33,7 @@ const MAX_INT64_NS = 9223372036854775807n; const UINT64_ACCUMULATOR_BOUND_NS = 1n << 63n; /** - * Parses a Go-style duration string to nanoseconds: a possibly-signed sequence of decimal + * Parses a duration string to nanoseconds: a possibly-signed sequence of decimal * numbers, each with a unit suffix (`"ns"`, `"us"`/`"µs"`/`"μs"`, `"ms"`, `"s"`, `"m"`, `"h"`), e.g. * `"5s"`, `"1h30m"`, `"300ms"`. Throws on invalid input, including an overflowing magnitude. * @@ -43,7 +42,7 @@ const UINT64_ACCUMULATOR_BOUND_NS = 1n << 63n; * ceiling, but every real config duration is nowhere near it, and the overflow check itself * doesn't depend on `number` precision. */ -export function parseGoDuration(value: string): number { +export function parseDuration(value: string): number { const orig = value; let s = value; let neg = false; @@ -139,11 +138,11 @@ export function parseGoDuration(value: string): number { } /** - * Formats nanoseconds as a Go-style duration string. `0` formats as `"0s"`; otherwise only the + * Formats nanoseconds as a duration string. `0` formats as `"0s"`; otherwise only the * needed units show, with minutes/seconds always trailing an hours component (`"1h0m0s"`), and a * sub-second remainder as a fraction of its largest applicable unit (`"1.5s"`, `"300ms"`). */ -export function formatGoDuration(nanoseconds: number): string { +export function formatDuration(nanoseconds: number): string { if (nanoseconds === 0) return "0s"; let ns = nanoseconds; @@ -202,10 +201,10 @@ function formatFraction(totalNs: number, unitNs: number): string { /** * Seconds form of a `Db.HealthTimeout` duration string (`"2m"` default). Throws on a malformed - * value, matching {@link parseGoDuration}; the caller wraps that into a typed config-load-failure + * value, matching {@link parseDuration}; the caller wraps that into a typed config-load-failure * error so rollback/cleanup still fires. A degenerate value like `"0s"` isn't special-cased: it * means exactly one immediate health probe with no wait, not a 30s fallback. */ export function resolveHealthTimeoutSeconds(healthTimeout: string): number { - return Math.trunc(parseGoDuration(healthTimeout) / 1_000_000_000); + return Math.trunc(parseDuration(healthTimeout) / 1_000_000_000); } diff --git a/apps/cli/src/command-internal/duration.unit.test.ts b/apps/cli/src/command-internal/duration.unit.test.ts new file mode 100644 index 0000000000..c1390e67b5 --- /dev/null +++ b/apps/cli/src/command-internal/duration.unit.test.ts @@ -0,0 +1,146 @@ +import { describe, expect, it } from "vitest"; + +import { formatDuration, parseDuration } from "./duration.ts"; + +describe("parseDuration", () => { + it("parses a single unit", () => { + expect(parseDuration("5s")).toBe(5_000_000_000); + expect(parseDuration("300ms")).toBe(300_000_000); + expect(parseDuration("1h")).toBe(3_600_000_000_000); + }); + + it("parses multiple units concatenated", () => { + expect(parseDuration("1h30m")).toBe(5_400_000_000_000); + }); + + it("parses a fractional value", () => { + expect(parseDuration("1.5s")).toBe(1_500_000_000); + }); + + it("parses a signed value", () => { + expect(parseDuration("-5s")).toBe(-5_000_000_000); + expect(parseDuration("+5s")).toBe(5_000_000_000); + }); + + it('treats a bare "0" as zero regardless of sign', () => { + expect(parseDuration("0")).toBe(0); + }); + + it("rejects an empty string", () => { + expect(() => parseDuration("")).toThrow('time: invalid duration ""'); + }); + + it.each(["s", "m", "h", "ms", "us", "µs", "μs", "ns"])( + 'rejects a bare unit with no preceding digit ("%s")', + (input) => { + expect(() => parseDuration(input)).toThrow(`time: invalid duration "${input}"`); + }, + ); + + it.each(["us", "µs", "μs"])('accepts every microsecond unit spelling ("1%s")', (unit) => { + expect(parseDuration(`1${unit}`)).toBe(1_000); + }); + + it('rejects a bare unit following a valid unit ("1hs")', () => { + expect(() => parseDuration("1hs")).toThrow('time: invalid duration "1hs"'); + }); + + it('rejects a negative bare unit ("-s")', () => { + expect(() => parseDuration("-s")).toThrow('time: invalid duration "-s"'); + }); + + it.each([".s", ".", "-.", "+.", ".h"])('rejects a lone "." with no digits ("%s")', (input) => { + expect(() => parseDuration(input)).toThrow(`time: invalid duration "${input}"`); + }); + + it('accepts a fraction-only value with no leading digit (".5s")', () => { + expect(parseDuration(".5s")).toBe(500_000_000); + }); + + it("rejects a number with no unit", () => { + expect(() => parseDuration("5")).toThrow('time: missing unit in duration "5"'); + }); + + it("rejects an unknown unit", () => { + expect(() => parseDuration("5x")).toThrow('time: unknown unit in duration "5x"'); + }); + + it('truncates a sub-nanosecond fraction instead of rounding ("0.5ns")', () => { + expect(parseDuration("0.5ns")).toBe(0); + }); + + it('truncates a sub-nanosecond fraction instead of rounding ("1.9ns")', () => { + expect(parseDuration("1.9ns")).toBe(1); + }); + + it('rounds a long fractional remainder up ("0.999999999999999999s")', () => { + expect(parseDuration("0.999999999999999999s")).toBe(1_000_000_000); + }); + + it('rejects a duration that overflows math.MaxInt64 nanoseconds ("2562048h")', () => { + expect(() => parseDuration("2562048h")).toThrow('time: invalid duration "2562048h"'); + }); + + it("accepts the maximum parseable duration without overflowing", () => { + expect(() => parseDuration("2562047h47m16.854775807s")).not.toThrow(); + }); + + it('rejects a duration exactly 1ns past the int64 maximum ("9223372036854775808ns")', () => { + expect(() => parseDuration("9223372036854775808ns")).toThrow( + 'time: invalid duration "9223372036854775808ns"', + ); + }); + + it("accepts a duration exactly at the int64 maximum in nanoseconds", () => { + expect(() => parseDuration("9223372036854775807ns")).not.toThrow(); + }); + + it("accepts the minimum representable negative duration (int64 minimum ns)", () => { + expect(parseDuration("-9223372036854775808ns")).toBe(-9223372036854775808); + }); + + it("accepts the equivalent hours/minutes/seconds form of math.MinInt64 ns", () => { + expect(parseDuration("-2562047h47m16.854775808s")).toBe(-9223372036854775808); + }); + + it("rejects a duration 1ns past math.MinInt64", () => { + expect(() => parseDuration("-9223372036854775809ns")).toThrow( + 'time: invalid duration "-9223372036854775809ns"', + ); + }); +}); + +describe("formatDuration", () => { + it('formats zero as "0s"', () => { + expect(formatDuration(0)).toBe("0s"); + }); + + it("formats whole hours/minutes/seconds", () => { + expect(formatDuration(3_600_000_000_000)).toBe("1h0m0s"); + }); + + it("normalizes 90s to 1m30s", () => { + expect(formatDuration(90_000_000_000)).toBe("1m30s"); + }); + + it("formats a sub-second fraction", () => { + expect(formatDuration(1_500_000_000)).toBe("1.5s"); + }); + + it("round-trips through parseDuration", () => { + expect(formatDuration(parseDuration("1h"))).toBe("1h0m0s"); + expect(formatDuration(parseDuration("90s"))).toBe("1m30s"); + }); + + it("includes a sub-second fraction alongside minutes", () => { + expect(formatDuration(parseDuration("1m0.5s"))).toBe("1m0.5s"); + }); + + it("includes a sub-second fraction alongside hours", () => { + expect(formatDuration(parseDuration("1h0.5s"))).toBe("1h0m0.5s"); + }); + + it("includes a sub-second fraction alongside hours and minutes", () => { + expect(formatDuration(parseDuration("1h1m0.5s"))).toBe("1h1m0.5s"); + }); +}); diff --git a/apps/cli/src/command-internal/edge-runtime-image.ts b/apps/cli/src/command-internal/edge-runtime-image.ts index b47205bd89..9b1972bddb 100644 --- a/apps/cli/src/command-internal/edge-runtime-image.ts +++ b/apps/cli/src/command-internal/edge-runtime-image.ts @@ -1,4 +1,4 @@ -import { legacyProjectStatePaths } from "./legacy-project-state.paths.ts"; +import { legacyProjectStatePaths } from "../shared/config/legacy-project-state.paths.ts"; import { usesStackState } from "./stack-backend.ts"; import { Effect, type FileSystem, Option, type Path } from "effect"; import { DENO1_EDGE_RUNTIME_VERSION } from "../shared/functions/functions.shared.ts"; diff --git a/apps/cli/src/command-internal/edge-runtime-script.layer.integration.test.ts b/apps/cli/src/command-internal/edge-runtime-script.layer.integration.test.ts index 1ac039779d..535e628a85 100644 --- a/apps/cli/src/command-internal/edge-runtime-script.layer.integration.test.ts +++ b/apps/cli/src/command-internal/edge-runtime-script.layer.integration.test.ts @@ -3,6 +3,7 @@ import { BunServices } from "@effect/platform-bun"; import { Effect, Exit, FileSystem, Layer, Option, Path } from "effect"; import { vi } from "vitest"; +import { cliConfigValuesTestLayer } from "../../tests/helpers/config-values-layer.ts"; import { DebugFlag, NetworkIdFlag } from "./global-flags.ts"; import { RuntimeInfo } from "../shared/runtime/runtime-info.service.ts"; import { runtimeInfoLayer } from "../shared/runtime/runtime-info.layer.ts"; @@ -10,7 +11,7 @@ import { CommandSettings } from "../config/command-settings.service.ts"; import { DockerRun, type DockerRunOpts } from "./docker-run.service.ts"; import { edgeRuntimeScriptLayer } from "./edge-runtime-script.layer.ts"; import { EdgeRuntimeScript } from "./edge-runtime-script.service.ts"; -import { projectStateModeLayer } from "./project-state-mode.ts"; +import { projectStateModeLayer } from "../shared/config/project-state-mode.ts"; import { mockCommandSettings } from "../../tests/helpers/command-mocks.ts"; import { seedLegacyStateDecoys } from "../../tests/helpers/legacy-state-guard.ts"; import { seedLock } from "../../tests/helpers/stack-lock.ts"; @@ -43,7 +44,6 @@ function fakeDocker(result: { exitCode: number; stdout?: string; stderr?: string // falls back to the default tag. function makeCliSettings(workdir = "/nonexistent-workdir") { return Layer.succeed(CommandSettings, { - dbPassword: Option.none(), githubToken: Option.none(), workdirEnvValue: Option.none(), profile: "supabase", @@ -82,6 +82,7 @@ function setup( Layer.succeed(DebugFlag, false), Layer.succeed(NetworkIdFlag, Option.none()), BunServices.layer, + cliConfigValuesTestLayer, ), ), ); diff --git a/apps/cli/src/command-internal/edge-runtime-script.layer.ts b/apps/cli/src/command-internal/edge-runtime-script.layer.ts index 94fb44e84d..9762108961 100644 --- a/apps/cli/src/command-internal/edge-runtime-script.layer.ts +++ b/apps/cli/src/command-internal/edge-runtime-script.layer.ts @@ -4,6 +4,7 @@ import * as Net from "node:net"; import { DebugFlag, NetworkIdFlag } from "./global-flags.ts"; import { RuntimeInfo } from "../shared/runtime/runtime-info.service.ts"; import { CommandSettings } from "../config/command-settings.service.ts"; +import { CliConfigValues } from "../config/cli-config-values.service.ts"; import { readDbToml } from "./db-config.toml-read.ts"; import { getRegistryImageUrl } from "./docker-registry.ts"; import { DockerRun } from "./docker-run.service.ts"; @@ -50,6 +51,7 @@ export const edgeRuntimeScriptLayer = Layer.effect( const debug = yield* DebugFlag; const networkIdFlag = yield* NetworkIdFlag; const runtimeInfo = yield* RuntimeInfo; + const configValues = yield* CliConfigValues; // The pg-delta container needs `host.docker.internal:host-gateway` on Linux only, so a // `host.docker.internal` local DB host (from SUPABASE_SERVICES_HOSTNAME) resolves inside the // container on Linux/dev-container; Docker Desktop already provides this on macOS/Windows. @@ -80,6 +82,7 @@ export const edgeRuntimeScriptLayer = Layer.effect( const denoVersion = opts.denoVersion ?? (yield* readDbToml(fs, path, workdir).pipe( + Effect.provideService(CliConfigValues, configValues), Effect.mapError((error) => new EdgeRuntimeScriptError({ message: error.message })), )).denoVersion; const registryImage = yield* getRegistryImageUrl( diff --git a/apps/cli/src/command-internal/edge-runtime-script.unit.test.ts b/apps/cli/src/command-internal/edge-runtime-script.unit.test.ts index 4a2ede6846..d7f3e67eff 100644 --- a/apps/cli/src/command-internal/edge-runtime-script.unit.test.ts +++ b/apps/cli/src/command-internal/edge-runtime-script.unit.test.ts @@ -16,7 +16,7 @@ describe("buildEdgeRuntimeStartCmd", () => { ]); }); - it("drops --port when allocation failed (Go preserves prior behaviour)", () => { + it("drops --port when allocation failed", () => { expect(buildEdgeRuntimeStartCmd({ port: Option.none(), debug: false })).toEqual([ "edge-runtime", "start", diff --git a/apps/cli/src/command-internal/experimental-gate.unit.test.ts b/apps/cli/src/command-internal/experimental-gate.unit.test.ts index 67d4b8324e..34ca11232c 100644 --- a/apps/cli/src/command-internal/experimental-gate.unit.test.ts +++ b/apps/cli/src/command-internal/experimental-gate.unit.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "@effect/vitest"; -import { ConfigProvider, Effect, Exit, Layer } from "effect"; +import { Effect, Exit, Layer } from "effect"; +import { withEnvVar } from "../../tests/helpers/command-mocks.ts"; import { CliArgs } from "../shared/cli/cli-args.service.ts"; import { ExperimentalFlag } from "./global-flags.ts"; import { ExperimentalRequiredError, requireExperimental } from "./experimental-gate.ts"; @@ -8,92 +9,87 @@ import { ExperimentalRequiredError, requireExperimental } from "./experimental-g const ENV = "SUPABASE_EXPERIMENTAL"; const withFlag = (value: boolean, args: ReadonlyArray = []) => Layer.mergeAll(Layer.succeed(ExperimentalFlag, value), Layer.succeed(CliArgs, { args })); -const withShellEnv = (env: Readonly>) => - ConfigProvider.layer(ConfigProvider.fromEnvRecord(env, { preserveEmptyStrings: true })); describe("requireExperimental", () => { it.effect("passes when --experimental is set", () => requireExperimental.pipe(Effect.provide(withFlag(true))), ); - it.effect("fails with Go's byte-exact message when neither flag nor env is set", () => + it.effect("fails with the gate message when neither flag nor env is set", () => Effect.gen(function* () { - const error = yield* requireExperimental.pipe( - Effect.provide(Layer.merge(withFlag(false), withShellEnv({}))), - Effect.flip, + const error = yield* withEnvVar( + ENV, + undefined, + requireExperimental.pipe(Effect.provide(withFlag(false)), Effect.flip), ); expect(error).toBeInstanceOf(ExperimentalRequiredError); expect(error.message).toBe("must set the --experimental flag to run this command"); }), ); - it.effect("passes when SUPABASE_EXPERIMENTAL=1 even without the flag (viper AutomaticEnv)", () => + it.effect("passes when SUPABASE_EXPERIMENTAL=1 even without the flag", () => Effect.gen(function* () { - const exit = yield* requireExperimental.pipe( - Effect.provide(Layer.merge(withFlag(false), withShellEnv({ [ENV]: "1" }))), - Effect.exit, + const exit = yield* withEnvVar( + ENV, + "1", + requireExperimental.pipe(Effect.provide(withFlag(false)), Effect.exit), ); expect(Exit.isSuccess(exit)).toBe(true); }), ); - it.effect( - "fails even with SUPABASE_EXPERIMENTAL=1 when --experimental=false is explicit (viper Changed wins)", - () => - Effect.gen(function* () { - const error = yield* requireExperimental.pipe( - Effect.provide( - Layer.merge(withFlag(false, ["--experimental=false"]), withShellEnv({ [ENV]: "1" })), - ), + it.effect("fails even with SUPABASE_EXPERIMENTAL=1 when --experimental=false is explicit", () => + Effect.gen(function* () { + const error = yield* withEnvVar( + ENV, + "1", + requireExperimental.pipe( + Effect.provide(withFlag(false, ["--experimental=false"])), Effect.flip, - ); - expect(error).toBeInstanceOf(ExperimentalRequiredError); - }), + ), + ); + expect(error).toBeInstanceOf(ExperimentalRequiredError); + }), ); it.effect( "passes with SUPABASE_EXPERIMENTAL=1 when --experimental=false is a positional operand after --", () => Effect.gen(function* () { - const exit = yield* requireExperimental.pipe( - Effect.provide( - Layer.merge( - withFlag(false, ["--", "--experimental=false"]), - withShellEnv({ [ENV]: "1" }), - ), + const exit = yield* withEnvVar( + ENV, + "1", + requireExperimental.pipe( + Effect.provide(withFlag(false, ["--", "--experimental=false"])), + Effect.exit, ), - Effect.exit, ); expect(Exit.isSuccess(exit)).toBe(true); }), ); - it.effect( - "a repeated --experimental=false --experimental=true keeps the LAST occurrence (viper Set() wins)", - () => - Effect.gen(function* () { - const exit = yield* requireExperimental.pipe( - Effect.provide( - withFlag(false, ["db", "pull", "--experimental=false", "--experimental=true"]), - ), - Effect.exit, - ); - expect(Exit.isSuccess(exit)).toBe(true); - }), + it.effect("a repeated --experimental=false --experimental=true keeps the LAST occurrence", () => + Effect.gen(function* () { + const exit = yield* requireExperimental.pipe( + Effect.provide( + withFlag(false, ["db", "pull", "--experimental=false", "--experimental=true"]), + ), + Effect.exit, + ); + expect(Exit.isSuccess(exit)).toBe(true); + }), ); - it.effect( - "a repeated --experimental=true --experimental=false keeps the LAST occurrence (viper Set() wins)", - () => - Effect.gen(function* () { - const error = yield* requireExperimental.pipe( - Effect.provide( - withFlag(true, ["db", "pull", "--experimental=true", "--experimental=false"]), - ), - Effect.flip, - ); - expect(error).toBeInstanceOf(ExperimentalRequiredError); - }), + it.effect("a repeated --experimental=true --experimental=false keeps the LAST occurrence", () => + Effect.gen(function* () { + const error = yield* requireExperimental.pipe( + Effect.provide( + withFlag(true, ["db", "pull", "--experimental=true", "--experimental=false"]), + ), + Effect.flip, + ); + expect(error).toBeInstanceOf(ExperimentalRequiredError); + }), ); it.effect("a repeated --experimental=false --experimental (bare) keeps the LAST occurrence", () => diff --git a/apps/cli/src/command-internal/fail-on.unit.test.ts b/apps/cli/src/command-internal/fail-on.unit.test.ts index 5bf34de451..497cdf2b10 100644 --- a/apps/cli/src/command-internal/fail-on.unit.test.ts +++ b/apps/cli/src/command-internal/fail-on.unit.test.ts @@ -35,7 +35,7 @@ describe("makeLevelEnum (exact-ci matcher — db advisors)", () => { expect(advisors.toEnum("ERROR")).toBe(2); }); - it("does NOT match a mixed-case level (Go's switch is exact)", () => { + it("does NOT match a mixed-case level (matching is exact)", () => { expect(advisors.toEnum("Info")).toBe(-1); expect(advisors.toEnum("warning")).toBe(-1); }); diff --git a/apps/cli/src/command-internal/go-float.ts b/apps/cli/src/command-internal/format-float.ts similarity index 61% rename from apps/cli/src/command-internal/go-float.ts rename to apps/cli/src/command-internal/format-float.ts index 286cef0f98..d180250dff 100644 --- a/apps/cli/src/command-internal/go-float.ts +++ b/apps/cli/src/command-internal/format-float.ts @@ -1,14 +1,14 @@ /** - * Formats a number the way Go's `fmt.Sprintf("%v", float64)` does: shortest `%g`, exponent - * notation when the decimal exponent is `< -4` or `>= 6` (`1000000` → `1e+06`), fixed notation - * otherwise, with a signed, at-least-two-digit exponent. + * Formats a number in its shortest round-trip form, switching to exponent notation when the + * decimal exponent is `< -4` or `>= 6` (`1000000` → `1e+06`), with a signed, at-least-two-digit + * exponent. * * Shared by `db query`'s value formatter and `postgres-config`'s pretty table. */ -export function goFormatFloat(n: number): string { +export function formatGeneralFloat(n: number): string { if (Number.isNaN(n)) return "NaN"; if (!Number.isFinite(n)) return n > 0 ? "+Inf" : "-Inf"; - // Go's `%v` preserves the sign of negative zero; `n === 0` is true for both `+0` and `-0`. + // Negative zero keeps its sign; `n === 0` is true for both `+0` and `-0`. if (Object.is(n, -0)) return "-0"; if (n === 0) return "0"; const neg = n < 0; diff --git a/apps/cli/src/command-internal/format-float.unit.test.ts b/apps/cli/src/command-internal/format-float.unit.test.ts new file mode 100644 index 0000000000..d82379056d --- /dev/null +++ b/apps/cli/src/command-internal/format-float.unit.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; + +import { formatGeneralFloat } from "./format-float.ts"; + +describe("formatGeneralFloat", () => { + it("renders fixed notation within the [-4, 6) decimal-exponent range", () => { + expect(formatGeneralFloat(100)).toBe("100"); + expect(formatGeneralFloat(100000)).toBe("100000"); + expect(formatGeneralFloat(0.5)).toBe("0.5"); + expect(formatGeneralFloat(0.0001)).toBe("0.0001"); + }); + + it("switches to signed exponent notation at exponent >= 6 or < -4", () => { + expect(formatGeneralFloat(1000000)).toBe("1e+06"); + expect(formatGeneralFloat(100000000000)).toBe("1e+11"); + expect(formatGeneralFloat(123456789)).toBe("1.23456789e+08"); + expect(formatGeneralFloat(0.00001)).toBe("1e-05"); + }); + + it("preserves the sign for negative exponent-notation values", () => { + expect(formatGeneralFloat(-1000000)).toBe("-1e+06"); + }); + + it("renders zero as a bare 0", () => { + expect(formatGeneralFloat(0)).toBe("0"); + }); +}); diff --git a/apps/cli/src/command-internal/functions-go-config.ts b/apps/cli/src/command-internal/functions-local-config.ts similarity index 51% rename from apps/cli/src/command-internal/functions-go-config.ts rename to apps/cli/src/command-internal/functions-local-config.ts index e0f053b1bc..8ded3eed41 100644 --- a/apps/cli/src/command-internal/functions-go-config.ts +++ b/apps/cli/src/command-internal/functions-local-config.ts @@ -1,5 +1,5 @@ import { Effect } from "effect"; -import type { FunctionsGoConfigCompat } from "../shared/functions/functions-config.ts"; +import type { FunctionsLocalConfigLoader } from "../shared/functions/functions-config.ts"; import { loadLocalProjectContext } from "./local-project-context.ts"; import { resolveLocalConfigValues } from "./local-config-values.ts"; @@ -17,7 +17,7 @@ function toError(cause: unknown): Error { * local-dev values (JWTs, URLs) are discarded here; only `projectId`/`edgeRuntimeDenoVersion` and * the validation side effect (fails on the first error) matter to these three commands. */ -export const functionsGoConfigCompat: FunctionsGoConfigCompat = { +export const functionsLocalConfigLoader: FunctionsLocalConfigLoader = { load: ({ projectRoot, projectRef }) => Effect.gen(function* () { const context = yield* loadLocalProjectContext(projectRoot, toError, projectRef); @@ -25,26 +25,17 @@ export const functionsGoConfigCompat: FunctionsGoConfigCompat = { context.config, context.hostname, projectRoot, - context.projectEnvValues, - context.loaded?.document, - // No `[remotes.]` override-tier gating (empty set, the default): the remote - // block already merged over the base config at file level via - // `loadLocalProjectContext`'s `projectRef` threading above. Known narrow divergence: - // an ambient `SUPABASE_EDGE_RUNTIME_DENO_VERSION` still beats a matched remote - // block's own `deno_version` here, since computing the override keys needs - // `db-config.toml-read.ts`'s remote-resolution pipeline, which this path doesn't run. - undefined, - projectRef, - ); + context.resolvedConfig.loaded.document, + ).pipe(Effect.mapError(toError)); return { - loaded: context.loaded, + loaded: { config: context.config, document: context.resolvedConfig.loaded.document }, + resolvedConfig: context.resolvedConfig, + configPath: context.resolvedConfig.hasConfigFile + ? context.resolvedConfig.loaded.path + : undefined, projectEnvValues: context.projectEnvValues, - // `context.projectId`, not `validated.projectId`: the context's id is the one built for - // Docker naming/labels — sanitized, `--project-ref` defaulted, and - // `SUPABASE_PROJECT_ID`-gated when a `[remotes.]` block matched (see - // `local-project-context.ts`'s gate). `validated.projectId` exists only to feed - // `validateResolvedConfig`'s emptiness check and skips that gate — see its own doc - // comment. + // `context.projectId` is the id built for Docker naming/labels; `validated.projectId` + // exists only to feed `validateResolvedConfig`'s emptiness check. projectId: context.projectId, denoVersion: validated.edgeRuntimeDenoVersion, }; diff --git a/apps/cli/src/command-internal/glob.unit.test.ts b/apps/cli/src/command-internal/glob.unit.test.ts index 0e79804e1a..a424671c2e 100644 --- a/apps/cli/src/command-internal/glob.unit.test.ts +++ b/apps/cli/src/command-internal/glob.unit.test.ts @@ -157,7 +157,7 @@ describe("walkSqlFiles", () => { }).pipe(Effect.provide(layer)); }); - it.effect("recurses into subdirectories, already sorted in Go's byte order", () => { + it.effect("recurses into subdirectories, already sorted in byte order", () => { const layer = fakeWalkFs( { "/schemas": ["nested", "top.sql"], "/schemas/nested": ["inner.sql"] }, { @@ -194,22 +194,19 @@ describe("walkSqlFiles", () => { }, ); - it.effect( - "does not descend into a symlinked subdirectory (Go's fs.WalkDir/afero.Walk no-follow)", - () => { - const layer = fakeWalkFs( - { "/schemas": ["linked", "top.sql"], "/schemas/linked": ["secret.sql"] }, - { "/schemas/linked": "Directory", "/schemas/top.sql": "File" }, - undefined, - new Set(["/schemas/linked"]), - ); - return Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const files = yield* walkSqlFiles(fs, "/schemas", ""); - expect([...files]).toEqual(["top.sql"]); - }).pipe(Effect.provide(layer)); - }, - ); + it.effect("does not descend into a symlinked subdirectory", () => { + const layer = fakeWalkFs( + { "/schemas": ["linked", "top.sql"], "/schemas/linked": ["secret.sql"] }, + { "/schemas/linked": "Directory", "/schemas/top.sql": "File" }, + undefined, + new Set(["/schemas/linked"]), + ); + return Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const files = yield* walkSqlFiles(fs, "/schemas", ""); + expect([...files]).toEqual(["top.sql"]); + }).pipe(Effect.provide(layer)); + }); it.effect("excludes a symlinked .sql file instead of applying its target", () => { const layer = fakeWalkFs( diff --git a/apps/cli/src/command-internal/global-flags.ts b/apps/cli/src/command-internal/global-flags.ts index df9c33e193..12723088bf 100644 --- a/apps/cli/src/command-internal/global-flags.ts +++ b/apps/cli/src/command-internal/global-flags.ts @@ -2,21 +2,22 @@ import { Effect, Option } from "effect"; import { Flag, GlobalFlag } from "effect/cli"; import { CliArgs } from "../shared/cli/cli-args.service.ts"; -import { VALUE_CONSUMING_LONG_FLAGS, VALUE_CONSUMING_SHORT_FLAGS } from "./db-target-flags.ts"; -import { viperEnvBool, viperEnvBoolWithProjectFallback } from "./viper-env.ts"; +import { + VALUE_CONSUMING_LONG_FLAGS, + VALUE_CONSUMING_SHORT_FLAGS, +} from "../shared/cli/value-consuming-flags.ts"; +import { envValue } from "../shared/config/env-option.ts"; +import { + parseEnvBool, + supabaseEnvBool, + supabaseEnvBoolWithProjectFallback, +} from "./supabase-env.ts"; +import { OFF_WORDS } from "./target/remote-vocabulary.ts"; -// The CLI's global-flag registry is tree-wide, so `-o/--output` can't be redeclared per command to -// vary its allowed values; this models it as the union of every command's accepted values, and -// each handler honors only the subset it cares about (e.g. `db query` reads `table`/`csv`; other -// commands ignore them and fall through to text). -// -// These flag description strings are user-visible in shell completion (`__complete` candidate -// descriptions), so keep them accurate when changing a flag. -/** - * Every value the global `-o/--output` flag accepts. Per-command `outputFormats` overrides (e.g. - * `config diff`'s handler-level rejection) widen against this list, so a value added here flows to - * those commands and their tests automatically. - */ +// `-o/--output` is tree-wide and can't vary per command, so it takes the union of every command's +// values and each handler honors its subset. +// Flag descriptions are user-visible in shell completion (`__complete`), so keep them accurate. +/** Every value `-o/--output` accepts; per-command `outputFormats` overrides widen against it. */ export const GLOBAL_OUTPUT_FORMATS = [ "env", "pretty", @@ -35,12 +36,7 @@ export const OutputFlag = GlobalFlag.Setting("output")({ ), }); -/** - * The TS-only `--output-format` global, accepted on any subcommand. - * - * A value-taking global flag must also be registered in `PERSISTENT_VALUE_FLAG_NAMES` - * (`shared/cli/cobra-flag-groups.ts`), or the pre-parse argv scanners won't consume its value. - */ +/** The TS-only `--output-format` global, accepted on any subcommand. */ export const OutputFormatFlag = GlobalFlag.Setting("output-format")({ flag: Flag.Literals("output-format", ["text", "json", "stream-json"]).pipe( Flag.withDescription("Output format: text (default), json, or stream-json (NDJSON)"), @@ -87,7 +83,9 @@ export const NetworkIdFlag = GlobalFlag.Setting("network-id")({ export const YesFlag = GlobalFlag.Setting("yes")({ flag: Flag.Boolean("yes").pipe( - Flag.withDescription("answer yes to all prompts"), + Flag.withDescription( + "answer yes to all prompts; hosted changes only accept this flag or SUPABASE_YES exported in your shell, never supabase/.env or another .env file", + ), Flag.withDefault(false), ), }); @@ -99,10 +97,13 @@ export const DnsResolverFlag = GlobalFlag.Setting("dns-resolver")({ ), }); +// Kept parsed (and hidden) only so passing it fails with the removal error from +// `withCommandTelemetry` instead of an unknown-flag parse error. export const CreateTicketFlag = GlobalFlag.Setting("create-ticket")({ flag: Flag.Boolean("create-ticket").pipe( - Flag.withDescription("create a support ticket for any CLI error"), + Flag.withDescription("Removed: report CLI problems with `supabase issue bug` instead."), Flag.withDefault(false), + Flag.withHidden, ), }); @@ -114,12 +115,8 @@ export const AgentFlag = GlobalFlag.Setting("agent")({ }); /** - * Every global/persistent flag declared above. - * - * A value-taking flag added here must also be added to `PERSISTENT_VALUE_FLAG_NAMES` - * (`shared/cli/cobra-flag-groups.ts`), which the handler-side pflag scans and pre-parse token - * scanners both derive their token set from. A flag missed there fails silently: an unregistered - * value flag won't consume its following token, so that token gets misread as positional. + * Every global flag declared above. A value-taking one must also join `PERSISTENT_VALUE_FLAG_NAMES` + * (`shared/cli/flag-groups.ts`), or its value token is silently misread as positional. */ export const GLOBAL_FLAGS = [ OutputFlag, @@ -138,14 +135,8 @@ export const GLOBAL_FLAGS = [ export const ROOT_GLOBAL_FLAGS = [OutputFormatFlag, ...GLOBAL_FLAGS] as const; /** - * Resolves the current value of every global/persistent flag above, keyed by its own CLI flag - * name. Used by `telemetry/command-telemetry.ts` as a fallback for a global flag not present in a - * handler's own `flags` record. - * - * Reads via `Effect.serviceOption` so a caller without the global-flag context (e.g. a focused - * unit test) gets an empty record instead of a missing-service defect. Reads each flag - * individually, since each `Setting`'s distinct value type can't be looped over without an - * `as` cast; `global-flags.unit.test.ts` keeps the resolved id set in sync with `GLOBAL_FLAGS`. + * Every global flag's current value by name, as telemetry's fallback for a flag missing from a + * handler's `flags`; empty when `serviceOption` finds no global-flag context. */ export const globalFlagValues = Effect.gen(function* () { const values: Record = {}; @@ -165,27 +156,16 @@ export const globalFlagValues = Effect.gen(function* () { return values; }); -const PFLAG_FALSE_VALUES = new Set(["0", "f", "F", "false", "FALSE", "False"]); +const FALSE_VALUES = new Set(["0", "f", "F", "false", "FALSE", "False"]); -/** - * Raw argv truncated at the first bare `--` operand terminator. This CLI's own lexer stops parsing - * flags there too, so everything after is a positional operand, e.g. a migration name literally - * called `--experimental=false` passed as `db pull -- --experimental=false`. The argv-scanning - * `*ExplicitlyFalse` heuristics below must only look at this region, or a positional operand that - * merely looks like a flag gets mistaken for an explicit one. - */ +/** Argv before the first bare `--`; the rest is an operand (`db pull -- --experimental=false`). */ const argsBeforeOperandTerminator = (args: ReadonlyArray): ReadonlyArray => { const terminatorIndex = args.indexOf("--"); return terminatorIndex === -1 ? args : args.slice(0, terminatorIndex); }; -/** - * Drops tokens that pflag would consume as a value-consuming flag's value in space-separated form - * (`--flag value` / `-f value`), so the `--yes`/`--experimental`/`--debug` argv scanners below - * don't mistake a consumed value token for an explicit occurrence of the global flag — e.g. - * `db pull --password --experimental=false` treats `--experimental=false` as `--password`'s - * value, not a changed `--experimental`. - */ +// Drops a flag's space-separated value, so `db pull --password --experimental=false` doesn't read +// as a changed `--experimental`. const nonValueConsumedTokens = (args: ReadonlyArray): ReadonlyArray => { const kept: Array = []; let skipNext = false; @@ -207,71 +187,78 @@ const nonValueConsumedTokens = (args: ReadonlyArray): ReadonlyArray`. A plain boolean flag can't - * distinguish an explicit `--yes=false` from the omitted default, so this scans the raw argv up to - * the first `--` operand terminator (see {@link argsBeforeOperandTerminator}), skipping tokens - * consumed as another flag's value (see {@link nonValueConsumedTokens}). + * The last `--yes`/`--yes=` in argv, or `undefined` if there is none. The parser keeps the + * first of a repeat and can't tell `--yes=false` from the default, so callers reread argv; a bare + * `--yes` reads a boolean literal that follows it, as the parser does. */ -const yesFlagExplicitlyFalse = (args: ReadonlyArray): boolean => - nonValueConsumedTokens(argsBeforeOperandTerminator(args)).some( - (arg) => arg.startsWith("--yes=") && PFLAG_FALSE_VALUES.has(arg.slice("--yes=".length)), - ); +const yesFlagFromArgs = (args: ReadonlyArray): boolean | undefined => { + const tokens = nonValueConsumedTokens(argsBeforeOperandTerminator(args)); + let result: boolean | undefined; + for (const [index, arg] of tokens.entries()) { + if (arg === "--yes") { + result = !OFF_WORDS.has(tokens[index + 1] ?? ""); + } else if (arg.startsWith("--yes=")) { + const value = arg.slice("--yes=".length); + result = !(FALSE_VALUES.has(value) || OFF_WORDS.has(value)); + } + } + return result; +}; -/** - * `--yes` resolved with an env fallback: when the flag isn't passed, `SUPABASE_YES` is honored. An - * explicit `--yes` (including `--yes=false`) wins over the env. Prefer this over reading - * {@link YesFlag} directly anywhere a command auto-confirms a prompt. - */ -export const resolveYes = Effect.gen(function* () { +export type YesSource = { readonly via: "flag" } | { readonly via: "env" }; + +/** The origin of a granted `--yes`, or `None`; an explicit `--yes=false` wins over the env. */ +export const resolveYesSource = Effect.gen(function* () { const flag = yield* YesFlag; const cliArgs = yield* CliArgs; - if (yesFlagExplicitlyFalse(cliArgs.args)) { - return false; + const explicit = yesFlagFromArgs(cliArgs.args); + if (explicit === false) { + return Option.none(); } - return flag || (yield* viperEnvBool("SUPABASE_YES")); + if (explicit ?? flag) return Option.some({ via: "flag" }); + const raw = yield* envValue("SUPABASE_YES"); + return raw !== undefined && parseEnvBool(raw) + ? Option.some({ via: "env" }) + : Option.none(); }); /** - * `--yes` resolved with the project `.env` consulted too, for commands that load the nested - * project env before prompting (`migration down`, `migration repair --all`). Shell env - * *presence* (any value) suppresses the file value entirely (see - * {@link viperEnvBoolWithProjectFallback}); an explicit `--yes` wins over both. `projectEnv` is - * the loaded map from `loadProjectEnv`. + * `--yes` with a `SUPABASE_YES` fallback; an explicit flag (including `--yes=false`) wins over the + * env. Prefer it to reading {@link YesFlag} directly. + */ +export const resolveYes = Effect.map(resolveYesSource, Option.isSome); + +/** + * {@link resolveYes} that also consults the project `.env` (the `loadProjectEnvValues` map) for + * commands that load it before prompting; shell env presence suppresses the file value (see + * {@link supabaseEnvBoolWithProjectFallback}), and an explicit `--yes` wins over both. */ export const resolveYesWithProjectEnv = (projectEnv: Record) => Effect.gen(function* () { const flag = yield* YesFlag; const cliArgs = yield* CliArgs; - if (yesFlagExplicitlyFalse(cliArgs.args)) { - return false; + const explicit = yesFlagFromArgs(cliArgs.args); + if (explicit !== undefined) { + return explicit; } - return flag || (yield* viperEnvBoolWithProjectFallback("SUPABASE_YES", projectEnv)); + return flag || (yield* supabaseEnvBoolWithProjectFallback("SUPABASE_YES", projectEnv)); }); -/** - * Resolves the raw argv's *last* explicit `--experimental` occurrence to a boolean, or `undefined` - * if it never appears before the first `--` operand terminator. Repeated occurrences resolve to - * the last one, the opposite of how this CLI's own flag parser resolves a repeat (first wins), so - * a caller must reread raw argv rather than trust the parsed flag. Tokens consumed as another - * flag's value are skipped (see {@link nonValueConsumedTokens}). - */ +// The last `--experimental`/`--experimental=` in argv, or `undefined`; the parser keeps the +// first of a repeat, so callers reread argv. const experimentalFlagFromArgs = (args: ReadonlyArray): boolean | undefined => { let result: boolean | undefined; for (const arg of nonValueConsumedTokens(argsBeforeOperandTerminator(args))) { if (arg === "--experimental") { result = true; } else if (arg.startsWith("--experimental=")) { - result = !PFLAG_FALSE_VALUES.has(arg.slice("--experimental=".length)); + result = !FALSE_VALUES.has(arg.slice("--experimental=".length)); } } return result; }; -/** - * `--experimental` resolved with an env fallback: `SUPABASE_EXPERIMENTAL` enables experimental - * commands when the flag isn't passed. An explicit `--experimental` (including - * `--experimental=false`, and the last of a repeated flag) wins over the env. - */ +/** `--experimental` with a `SUPABASE_EXPERIMENTAL` fallback; an explicit flag, even false, wins. */ export const resolveExperimental = Effect.gen(function* () { const flag = yield* ExperimentalFlag; const cliArgs = yield* CliArgs; @@ -279,13 +266,10 @@ export const resolveExperimental = Effect.gen(function* () { if (explicit !== undefined) { return explicit; } - return flag || (yield* viperEnvBool("SUPABASE_EXPERIMENTAL")); + return flag || (yield* supabaseEnvBool("SUPABASE_EXPERIMENTAL")); }); -/** - * `--experimental` with project `.env` fallback. Shell-env presence wins over the file; - * an explicit flag wins over both. See {@link viperEnvBoolWithProjectFallback}. - */ +/** {@link resolveExperimental} plus project `.env`: shell env beats the file, a flag beats both. */ export const resolveExperimentalWithProjectEnv = (projectEnv: Record) => Effect.gen(function* () { const flag = yield* ExperimentalFlag; @@ -294,34 +278,26 @@ export const resolveExperimentalWithProjectEnv = (projectEnv: Record` occurrence in argv resolves to false. Tracks the - * last occurrence (unlike {@link yesFlagExplicitlyFalse}'s "any occurrence" check), since a - * trailing bare `--debug` after an earlier `--debug=false` must win. Scans only the flag-parsing - * region (see {@link argsBeforeOperandTerminator}) and skips tokens consumed as another flag's - * value (see {@link nonValueConsumedTokens}). - */ +// True when the last `--debug`/`--debug=` in argv resolves to false: a trailing bare +// `--debug` must beat an earlier `--debug=false`. const debugFlagExplicitlyFalse = (args: ReadonlyArray): boolean => { let lastExplicitlyFalse = false; for (const arg of nonValueConsumedTokens(argsBeforeOperandTerminator(args))) { if (arg === "--debug") { lastExplicitlyFalse = false; } else if (arg.startsWith("--debug=")) { - lastExplicitlyFalse = PFLAG_FALSE_VALUES.has(arg.slice("--debug=".length)); + lastExplicitlyFalse = FALSE_VALUES.has(arg.slice("--debug=".length)); } } return lastExplicitlyFalse; }; /** - * `--debug` resolved with an env fallback, and the project `.env` consulted too, for debug-gated - * behavior downstream of a command that already loaded the nested project env. `loadProjectEnv` - * is pure, so callers pass the loaded map through explicitly — same shape as - * {@link resolveYesWithProjectEnv}. Shell env *presence* suppresses the file value; an explicit - * `--debug` wins over both. + * `--debug` with env and project `.env` fallbacks, for debug-gated code downstream of a command + * that already loaded the project env. */ export const resolveDebugWithProjectEnv = (projectEnv: Record) => Effect.gen(function* () { @@ -330,5 +306,5 @@ export const resolveDebugWithProjectEnv = (projectEnv: Record) = if (debugFlagExplicitlyFalse(cliArgs.args)) { return false; } - return flag || (yield* viperEnvBoolWithProjectFallback("SUPABASE_DEBUG", projectEnv)); + return flag || (yield* supabaseEnvBoolWithProjectFallback("SUPABASE_DEBUG", projectEnv)); }); diff --git a/apps/cli/src/command-internal/global-flags.unit.test.ts b/apps/cli/src/command-internal/global-flags.unit.test.ts index f6153d82a2..390f3fa4cf 100644 --- a/apps/cli/src/command-internal/global-flags.unit.test.ts +++ b/apps/cli/src/command-internal/global-flags.unit.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "@effect/vitest"; import { Effect, Layer, Option } from "effect"; +import { withConfigEnv, withEmptyConfigEnv } from "../../tests/helpers/command-mocks.ts"; import { CliArgs } from "../shared/cli/cli-args.service.ts"; import { GLOBAL_FLAGS, @@ -15,6 +16,8 @@ import { YesFlag, globalFlagValues, resolveDebugWithProjectEnv, + resolveYesSource, + resolveYesWithProjectEnv, } from "./global-flags.ts"; describe("globalFlagValues", () => { @@ -120,3 +123,89 @@ describe("resolveDebugWithProjectEnv", () => { }, ); }); + +// `parsed` is what the flag parser yields, which keeps the first occurrence of a repeated flag. +describe("resolveYesSource", () => { + const resolve = ( + args: ReadonlyArray, + { parsed, shellYes }: { readonly parsed: boolean; readonly shellYes?: string }, + ) => + withEmptyConfigEnv( + withConfigEnv(shellYes === undefined ? {} : { SUPABASE_YES: shellYes }, resolveYesSource), + ).pipe( + Effect.provide( + Layer.mergeAll(Layer.succeed(YesFlag, parsed), Layer.succeed(CliArgs, { args })), + ), + ); + + it.effect.each([ + { args: ["--yes=false", "--yes"], parsed: false }, + { args: ["--yes=false", "--yes=true"], parsed: false }, + { args: ["--yes", "false", "--yes"], parsed: false }, + { args: ["--yes", "--", "--yes=false"], parsed: true }, + { args: ["--yes", "--password", "--yes=false"], parsed: true }, + ])("confirms $args because the last --yes is true", ({ args, parsed }) => + Effect.gen(function* () { + expect(yield* resolve(args, { parsed })).toEqual(Option.some({ via: "flag" })); + }), + ); + + it.effect.each([ + { args: ["--yes", "--yes=false"], parsed: true }, + { args: ["--yes=true", "--yes=false"], parsed: true }, + { args: ["--yes=false"], parsed: false }, + { args: ["--yes", "false"], parsed: false }, + { args: ["--yes=no"], parsed: false }, + { args: ["--yes", "off"], parsed: false }, + ])( + "denies $args because the last --yes is false, even with SUPABASE_YES set", + ({ args, parsed }) => + Effect.gen(function* () { + expect(yield* resolve(args, { parsed, shellYes: "1" })).toEqual(Option.none()); + }), + ); + + it.effect("falls back to a shell SUPABASE_YES when --yes never appears", () => + Effect.gen(function* () { + expect(yield* resolve([], { parsed: false, shellYes: "1" })).toEqual( + Option.some({ via: "env" }), + ); + expect(yield* resolve([], { parsed: false })).toEqual(Option.none()); + }), + ); +}); + +describe("resolveYesWithProjectEnv", () => { + const resolve = ( + args: ReadonlyArray, + { + parsed, + projectEnv, + }: { readonly parsed: boolean; readonly projectEnv: Record }, + ) => + withEmptyConfigEnv(resolveYesWithProjectEnv(projectEnv)).pipe( + Effect.provide( + Layer.mergeAll(Layer.succeed(YesFlag, parsed), Layer.succeed(CliArgs, { args })), + ), + ); + + it.effect("lets the last --yes decide, in either order", () => + Effect.gen(function* () { + expect(yield* resolve(["--yes=false", "--yes"], { parsed: false, projectEnv: {} })).toBe( + true, + ); + expect( + yield* resolve(["--yes", "--yes=false"], { + parsed: true, + projectEnv: { SUPABASE_YES: "1" }, + }), + ).toBe(false); + }), + ); + + it.effect("falls back to the project env when --yes never appears", () => + Effect.gen(function* () { + expect(yield* resolve([], { parsed: false, projectEnv: { SUPABASE_YES: "1" } })).toBe(true); + }), + ); +}); diff --git a/apps/cli/src/command-internal/go-child-exit.error.ts b/apps/cli/src/command-internal/go-child-exit.error.ts deleted file mode 100644 index 511f1a8d9f..0000000000 --- a/apps/cli/src/command-internal/go-child-exit.error.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Data, Runtime } from "effect"; - -import { - actionability, - type CliErrorActionabilityDeclaration, - ErrorActionabilityId, -} from "../shared/telemetry/error-actionability.ts"; - -/** - * A spawned `supabase-go` child process exited non-zero, or couldn't be spawned at all. - * - * Carries the exit code through `Runtime.errorExitCode` so it survives every `Effect.ensuring` - * finalizer up to `runCli`'s own exit call, instead of exiting the process directly and skipping - * them. `runCli` also special-cases this class to skip its own generic stderr line, since the - * child already wrote its own failure detail there. `exitCode` must be a real non-zero status; - * every construction site guards this before creating the error. - */ -export class GoChildExitError extends Data.TaggedError("GoChildExitError")<{ - readonly exitCode: number; - readonly message: string; -}> { - override readonly [Runtime.errorExitCode] = this.exitCode; - - get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { - return actionability.unknown; - } -} diff --git a/apps/cli/src/command-internal/go-duration.unit.test.ts b/apps/cli/src/command-internal/go-duration.unit.test.ts deleted file mode 100644 index b7b58558a0..0000000000 --- a/apps/cli/src/command-internal/go-duration.unit.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { formatGoDuration, parseGoDuration } from "./go-duration.ts"; - -describe("parseGoDuration", () => { - it("parses a single unit", () => { - expect(parseGoDuration("5s")).toBe(5_000_000_000); - expect(parseGoDuration("300ms")).toBe(300_000_000); - expect(parseGoDuration("1h")).toBe(3_600_000_000_000); - }); - - it("parses multiple units concatenated", () => { - expect(parseGoDuration("1h30m")).toBe(5_400_000_000_000); - }); - - it("parses a fractional value", () => { - expect(parseGoDuration("1.5s")).toBe(1_500_000_000); - }); - - it("parses a signed value", () => { - expect(parseGoDuration("-5s")).toBe(-5_000_000_000); - expect(parseGoDuration("+5s")).toBe(5_000_000_000); - }); - - it('treats a bare "0" as zero regardless of sign', () => { - expect(parseGoDuration("0")).toBe(0); - }); - - it("rejects an empty string", () => { - expect(() => parseGoDuration("")).toThrow('time: invalid duration ""'); - }); - - it.each(["s", "m", "h", "ms", "us", "µs", "μs", "ns"])( - 'rejects a bare unit with no preceding digit ("%s")', - (input) => { - expect(() => parseGoDuration(input)).toThrow(`time: invalid duration "${input}"`); - }, - ); - - it.each(["us", "µs", "μs"])('accepts every microsecond unit spelling ("1%s")', (unit) => { - expect(parseGoDuration(`1${unit}`)).toBe(1_000); - }); - - it('rejects a bare unit following a valid unit ("1hs")', () => { - expect(() => parseGoDuration("1hs")).toThrow('time: invalid duration "1hs"'); - }); - - it('rejects a negative bare unit ("-s")', () => { - expect(() => parseGoDuration("-s")).toThrow('time: invalid duration "-s"'); - }); - - it.each([".s", ".", "-.", "+.", ".h"])('rejects a lone "." with no digits ("%s")', (input) => { - expect(() => parseGoDuration(input)).toThrow(`time: invalid duration "${input}"`); - }); - - it('accepts a fraction-only value with no leading digit (".5s")', () => { - expect(parseGoDuration(".5s")).toBe(500_000_000); - }); - - it("rejects a number with no unit", () => { - expect(() => parseGoDuration("5")).toThrow('time: missing unit in duration "5"'); - }); - - it("rejects an unknown unit", () => { - expect(() => parseGoDuration("5x")).toThrow('time: unknown unit in duration "5x"'); - }); - - it('truncates a sub-nanosecond fraction instead of rounding ("0.5ns")', () => { - expect(parseGoDuration("0.5ns")).toBe(0); - }); - - it('truncates a sub-nanosecond fraction instead of rounding ("1.9ns")', () => { - expect(parseGoDuration("1.9ns")).toBe(1); - }); - - it('rounds a long fractional remainder up like Go\'s float64 conversion ("0.999999999999999999s")', () => { - expect(parseGoDuration("0.999999999999999999s")).toBe(1_000_000_000); - }); - - it('rejects a duration that overflows math.MaxInt64 nanoseconds ("2562048h")', () => { - expect(() => parseGoDuration("2562048h")).toThrow('time: invalid duration "2562048h"'); - }); - - it("accepts Go's actual maximum parseable duration without overflowing", () => { - expect(() => parseGoDuration("2562047h47m16.854775807s")).not.toThrow(); - }); - - it('rejects a duration exactly 1ns past Go\'s true math.MaxInt64 ceiling ("9223372036854775808ns")', () => { - expect(() => parseGoDuration("9223372036854775808ns")).toThrow( - 'time: invalid duration "9223372036854775808ns"', - ); - }); - - it("accepts a duration exactly at Go's true math.MaxInt64 ceiling in nanoseconds", () => { - expect(() => parseGoDuration("9223372036854775807ns")).not.toThrow(); - }); - - it("accepts Go's exact minimum representable negative duration (math.MinInt64 ns)", () => { - expect(parseGoDuration("-9223372036854775808ns")).toBe(-9223372036854775808); - }); - - it("accepts the equivalent hours/minutes/seconds form of math.MinInt64 ns", () => { - expect(parseGoDuration("-2562047h47m16.854775808s")).toBe(-9223372036854775808); - }); - - it("rejects a duration 1ns past math.MinInt64", () => { - expect(() => parseGoDuration("-9223372036854775809ns")).toThrow( - 'time: invalid duration "-9223372036854775809ns"', - ); - }); -}); - -describe("formatGoDuration", () => { - it('formats zero as "0s"', () => { - expect(formatGoDuration(0)).toBe("0s"); - }); - - it("formats whole hours/minutes/seconds", () => { - expect(formatGoDuration(3_600_000_000_000)).toBe("1h0m0s"); - }); - - it("normalizes 90s to 1m30s", () => { - expect(formatGoDuration(90_000_000_000)).toBe("1m30s"); - }); - - it("formats a sub-second fraction", () => { - expect(formatGoDuration(1_500_000_000)).toBe("1.5s"); - }); - - it("round-trips through parseGoDuration", () => { - expect(formatGoDuration(parseGoDuration("1h"))).toBe("1h0m0s"); - expect(formatGoDuration(parseGoDuration("90s"))).toBe("1m30s"); - }); - - it("includes a sub-second fraction alongside minutes", () => { - expect(formatGoDuration(parseGoDuration("1m0.5s"))).toBe("1m0.5s"); - }); - - it("includes a sub-second fraction alongside hours", () => { - expect(formatGoDuration(parseGoDuration("1h0.5s"))).toBe("1h0m0.5s"); - }); - - it("includes a sub-second fraction alongside hours and minutes", () => { - expect(formatGoDuration(parseGoDuration("1h1m0.5s"))).toBe("1h1m0.5s"); - }); -}); diff --git a/apps/cli/src/command-internal/go-float.unit.test.ts b/apps/cli/src/command-internal/go-float.unit.test.ts deleted file mode 100644 index 7ebaa15172..0000000000 --- a/apps/cli/src/command-internal/go-float.unit.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { goFormatFloat } from "./go-float.ts"; - -describe("goFormatFloat", () => { - it("renders fixed notation within Go's [-4, 6) decimal-exponent range", () => { - expect(goFormatFloat(100)).toBe("100"); - expect(goFormatFloat(100000)).toBe("100000"); - expect(goFormatFloat(0.5)).toBe("0.5"); - expect(goFormatFloat(0.0001)).toBe("0.0001"); - }); - - it("switches to signed exponent notation at exponent >= 6 or < -4", () => { - expect(goFormatFloat(1000000)).toBe("1e+06"); - expect(goFormatFloat(100000000000)).toBe("1e+11"); - expect(goFormatFloat(123456789)).toBe("1.23456789e+08"); - expect(goFormatFloat(0.00001)).toBe("1e-05"); - }); - - it("preserves the sign for negative exponent-notation values", () => { - expect(goFormatFloat(-1000000)).toBe("-1e+06"); - }); - - it("renders zero as a bare 0", () => { - expect(goFormatFloat(0)).toBe("0"); - }); -}); diff --git a/apps/cli/src/command-internal/go-json.unit.test.ts b/apps/cli/src/command-internal/go-json.unit.test.ts deleted file mode 100644 index d00c4618f3..0000000000 --- a/apps/cli/src/command-internal/go-json.unit.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { - encodeGoJsonCompact, - encodeGoJsonIndented, - escapeGoJsonString, - goJsonKindName, -} from "./go-json.ts"; - -describe("escapeGoJsonString", () => { - it("escapes quotes and backslashes like Go", () => { - expect(escapeGoJsonString(`a"b\\c`)).toBe('"a\\"b\\\\c"'); - }); - - it("HTML-escapes <, > and & (Go's default escapeHTML)", () => { - expect(escapeGoJsonString(" & ")).toBe('"\\u003ca\\u003e \\u0026 \\u003cb\\u003e"'); - }); - - it("uses short escapes for tab/newline/carriage-return", () => { - expect(escapeGoJsonString("a\tb\nc\rd")).toBe('"a\\tb\\nc\\rd"'); - }); - - it("uses \\u00xx for other control characters (no \\b / \\f shorthand)", () => { - expect(escapeGoJsonString("\b\f")).toBe('"\\u0008\\u000c"'); - }); - - it("escapes U+2028 and U+2029", () => { - expect(escapeGoJsonString("

")).toBe('"\\u2028\\u2029"'); - }); -}); - -describe("encodeGoJsonIndented", () => { - it("preserves object key insertion order (not alphabetical)", () => { - expect(encodeGoJsonIndented({ level: "error", message: "boom" })).toBe( - `{\n "level": "error",\n "message": "boom"\n}\n`, - ); - }); - - it("renders nested arrays of objects with 2-space indent and a trailing newline", () => { - const value = [{ function: "public.f1", issues: [{ level: "error", message: "test 1b" }] }]; - expect(encodeGoJsonIndented(value)).toBe( - [ - "[", - " {", - ' "function": "public.f1",', - ' "issues": [', - " {", - ' "level": "error",', - ' "message": "test 1b"', - " }", - " ]", - " }", - "]", - "", - ].join("\n"), - ); - }); - - it("renders empty arrays and objects compactly", () => { - expect(encodeGoJsonIndented([])).toBe("[]\n"); - expect(encodeGoJsonIndented({})).toBe("{}\n"); - expect(encodeGoJsonIndented({ issues: [] })).toBe(`{\n "issues": []\n}\n`); - }); -}); - -describe("encodeGoJsonCompact", () => { - it("matches Go's json.Marshal compact shape with HTML escaping", () => { - expect(encodeGoJsonCompact({ metadata_xml: "&stuff", type: "saml" })).toBe( - '{"metadata_xml":"\\u003cxml\\u003e\\u0026stuff\\u003c/xml\\u003e","type":"saml"}', - ); - }); - - it("keeps insertion order, compact separators, and no trailing newline", () => { - expect(encodeGoJsonCompact({ b: [1, 2], a: { c: true } })).toBe('{"b":[1,2],"a":{"c":true}}'); - expect(encodeGoJsonCompact([])).toBe("[]"); - expect(encodeGoJsonCompact(null)).toBe("null"); - }); - - it("preserves negative zero's sign, unlike plain JSON.stringify", () => { - expect(encodeGoJsonCompact({ extra: -0 })).toBe('{"extra":-0}'); - expect(encodeGoJsonCompact({ extra: Number("-1e-10000") })).toBe('{"extra":-0}'); - expect(encodeGoJsonCompact({ extra: 0 })).toBe('{"extra":0}'); - }); - - it("iterates a Map in true insertion order, unlike a plain object with integer-like keys", () => { - const map = new Map([ - ["10", "a"], - ["2", "b"], - ]); - expect(encodeGoJsonCompact(map)).toBe('{"10":"a","2":"b"}'); - }); -}); - -describe("goJsonKindName", () => { - it("names every JSON-representable kind, including the generic fallback", () => { - expect(goJsonKindName([])).toBe("array"); - expect(goJsonKindName(1)).toBe("number"); - expect(goJsonKindName("s")).toBe("string"); - expect(goJsonKindName(true)).toBe("bool"); - expect(goJsonKindName(undefined)).toBe("value"); - }); -}); diff --git a/apps/cli/src/command-internal/go-proxy-invocation.ts b/apps/cli/src/command-internal/go-proxy-invocation.ts deleted file mode 100644 index eb0abfe66f..0000000000 --- a/apps/cli/src/command-internal/go-proxy-invocation.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Context, Effect, Layer, Ref } from "effect"; - -interface GoProxyInvocationShape { - readonly markDelegated: Effect.Effect; - readonly wasDelegated: Effect.Effect; -} - -export class GoProxyInvocation extends Context.Service()( - "supabase/cli/GoProxyInvocation", -) {} - -export const goProxyInvocationLayer = Layer.effect( - GoProxyInvocation, - Effect.gen(function* () { - const delegated = yield* Ref.make(false); - return GoProxyInvocation.of({ - markDelegated: Ref.set(delegated, true), - wasDelegated: Ref.get(delegated), - }); - }), -); diff --git a/apps/cli/src/command-internal/go-proxy-target-env.integration.test.ts b/apps/cli/src/command-internal/go-proxy-target-env.integration.test.ts deleted file mode 100644 index b01cc5afe8..0000000000 --- a/apps/cli/src/command-internal/go-proxy-target-env.integration.test.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { describe, expect, it } from "@effect/vitest"; -import { BunServices } from "@effect/platform-bun"; -import { Effect, FileSystem, Layer, Option, Path, Schema } from "effect"; - -import { useTempWorkdir, VALID_TOKEN } from "../../tests/helpers/command-mocks.ts"; -import { isolatedHomeLayer } from "../../tests/helpers/command-mocks.ts"; -import { - mockAnalytics, - mockOutput, - mockTelemetryRuntime, - mockTty, -} from "../../tests/helpers/mocks.ts"; -import { - projectResponse, - provideEnv, - stubManagementApi, -} from "../../tests/helpers/permission-real-layers.ts"; -import { dbRemoteChanges } from "../commands/db/remote/changes/changes.handler.ts"; -import { genKeys } from "../commands/gen/keys/keys.handler.ts"; -import { PROJECT_NOT_LINKED_MESSAGE } from "../config/remote-target.service.ts"; -import { CliArgs } from "../shared/cli/cli-args.service.ts"; -import { - DebugFlag, - DnsResolverFlag, - OutputFlag, - ProfileFlag, - WorkdirFlag, -} from "./global-flags.ts"; -import { GoProxy } from "./go-proxy.service.ts"; -import { projectStateModeLayer, type ProjectStateMode } from "./project-state-mode.ts"; - -const LINKED_REF = "abcdefghijklmnopqrst"; - -const setup = ( - workdir: string, - mode: ProjectStateMode, - profile = "supabase", - interactive?: { readonly chosenRef: string }, -) => { - const calls: Array<{ readonly args: ReadonlyArray }> = []; - const poolerUrls: Array = []; - const layer = Layer.mergeAll( - BunServices.layer, - isolatedHomeLayer(workdir, { - SUPABASE_NO_KEYRING: "1", - ...(interactive === undefined ? {} : { SUPABASE_ACCESS_TOKEN: VALID_TOKEN }), - }), - projectStateModeLayer(mode), - mockTty({ stdinIsTty: interactive !== undefined }), - mockOutput({ - promptSelectResponses: interactive === undefined ? [] : [interactive.chosenRef], - }).layer, - mockAnalytics().layer, - mockTelemetryRuntime(), - Layer.succeed(CliArgs, { args: [] }), - Layer.succeed(DebugFlag, false), - Layer.succeed(ProfileFlag, profile), - Layer.succeed(WorkdirFlag, Option.some(workdir)), - Layer.succeed(OutputFlag, Option.none()), - Layer.succeed(DnsResolverFlag, "native"), - Layer.succeed(GoProxy, { - exec: (args, execOpts) => - Effect.sync(() => { - calls.push({ args }); - poolerUrls.push(execOpts?.env?.SUPABASE_INTERNAL_POOLER_URL); - }), - execCapture: () => Effect.die("the delegated commands do not capture output"), - }), - ); - return { calls, poolerUrls, layer }; -}; - -const POOLER_URL = - "postgresql://postgres.abcdefghijklmnopqrst:[YOUR-PASSWORD]@pooler.example.com:6543/postgres"; - -const writeStackLink = Effect.fnUntraced(function* (workdir: string, poolerUrl?: string) { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - yield* fs.makeDirectory(path.join(workdir, "supabase"), { recursive: true }); - yield* fs.writeFileString( - path.join(workdir, "supabase", ".link.json"), - yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({ - project: { ref: LINKED_REF }, - active_branch: { - ref: LINKED_REF, - ...(poolerUrl === undefined ? {} : { pooler_url: poolerUrl }), - }, - fetchedAt: "2026-01-01T00:00:00.000Z", - }), - ); -}); - -const changesFlags = (overrides: { dbUrl?: Option.Option } = {}) => ({ - schema: [], - dbUrl: overrides.dbUrl ?? Option.none(), - linked: false, - password: Option.none(), -}); - -describe("Go-delegated linked target", () => { - const tmp = useTempWorkdir("supabase-go-target-env-"); - const env = { SUPABASE_NO_KEYRING: "1", SUPABASE_HOME: undefined }; - - it.effect("passes the linked ref to `db remote changes` as --project-ref in stack mode", () => { - const { calls, layer } = setup(tmp.current, "stack"); - return Effect.gen(function* () { - yield* writeStackLink(tmp.current); - yield* dbRemoteChanges(changesFlags()); - expect(calls).toEqual([{ args: ["db", "remote", "changes", "--project-ref", LINKED_REF] }]); - }).pipe(Effect.provide(layer), provideEnv(env)); - }); - - it.effect("passes no ref when `db remote changes` has its own --db-url", () => { - const { calls, layer } = setup(tmp.current, "stack"); - return Effect.gen(function* () { - yield* writeStackLink(tmp.current); - yield* dbRemoteChanges(changesFlags({ dbUrl: Option.some("postgres://x@localhost/db") })); - expect(calls).toEqual([ - { - args: ["db", "remote", "changes", "--db-url", "postgres://x@localhost/db"], - }, - ]); - }).pipe(Effect.provide(layer), provideEnv(env)); - }); - - it.effect("adds no ref and builds no target runtime in legacy mode", () => { - // An unknown profile fails the settings layer, so a legacy run that succeeds never built it. - const { calls, layer } = setup(tmp.current, "legacy", "no-such-profile"); - return Effect.gen(function* () { - yield* dbRemoteChanges(changesFlags()); - yield* genKeys({ projectRef: Option.none(), overrideName: [] }); - expect(calls).toEqual([{ args: ["db", "remote", "changes"] }, { args: ["gen", "keys"] }]); - }).pipe(Effect.provide(layer), provideEnv(env)); - }); - - it.effect("passes the linked ref to `gen keys` as --project-ref in stack mode", () => { - const { calls, layer } = setup(tmp.current, "stack"); - return Effect.gen(function* () { - yield* writeStackLink(tmp.current); - yield* genKeys({ projectRef: Option.none(), overrideName: [] }); - expect(calls).toEqual([{ args: ["gen", "keys", "--project-ref", LINKED_REF] }]); - }).pipe(Effect.provide(layer), provideEnv(env)); - }); - - it.effect("prompts for a project in `gen keys` when stack mode has nothing linked", () => { - const { calls, layer } = setup(tmp.current, "stack", "supabase", { chosenRef: LINKED_REF }); - const requests: Array<{ readonly method: string; readonly path: string }> = []; - return Effect.gen(function* () { - yield* stubManagementApi(requests, (method, path) => - method === "GET" && path === "/v1/projects" - ? Response.json([projectResponse(LINKED_REF)]) - : undefined, - ); - yield* genKeys({ projectRef: Option.none(), overrideName: [] }); - expect(requests).toEqual([{ method: "GET", path: "/v1/projects" }]); - expect(calls).toEqual([{ args: ["gen", "keys", "--project-ref", LINKED_REF] }]); - }).pipe(Effect.scoped, Effect.provide(layer), provideEnv(env)); - }); - - it.effect("keeps the link ahead of the picker in `gen keys`", () => { - const { calls, layer } = setup(tmp.current, "stack", "supabase", { - chosenRef: "zyxwvutsrqponmlkjihg", - }); - return Effect.gen(function* () { - yield* writeStackLink(tmp.current); - yield* genKeys({ projectRef: Option.none(), overrideName: [] }); - expect(calls).toEqual([{ args: ["gen", "keys", "--project-ref", LINKED_REF] }]); - }).pipe(Effect.provide(layer), provideEnv(env)); - }); - - it.effect( - "fails `db remote changes` without prompting when stack mode has nothing linked", - () => { - const { calls, layer } = setup(tmp.current, "stack", "supabase", { chosenRef: LINKED_REF }); - const requests: Array<{ readonly method: string; readonly path: string }> = []; - return Effect.gen(function* () { - yield* stubManagementApi(requests, () => undefined); - const error = yield* dbRemoteChanges(changesFlags()).pipe(Effect.flip); - expect(error).toMatchObject({ - _tag: "ProjectRefNotLinkedError", - message: PROJECT_NOT_LINKED_MESSAGE, - }); - expect(requests).toEqual([]); - expect(calls).toEqual([]); - }).pipe(Effect.scoped, Effect.provide(layer), provideEnv(env)); - }, - ); - - it.effect("passes an explicit --project-ref to `gen keys` in stack mode", () => { - const { calls, layer } = setup(tmp.current, "stack"); - const explicitRef = "zyxwvutsrqponmlkjihg"; - return Effect.gen(function* () { - yield* writeStackLink(tmp.current); - yield* genKeys({ projectRef: Option.some(explicitRef), overrideName: [] }); - expect(calls).toEqual([{ args: ["gen", "keys", "--project-ref", explicitRef] }]); - }).pipe(Effect.provide(layer), provideEnv(env)); - }); - - it.effect("forwards the linked pooler URL with the linked ref", () => { - const { calls, poolerUrls, layer } = setup(tmp.current, "stack"); - return Effect.gen(function* () { - yield* writeStackLink(tmp.current, POOLER_URL); - yield* dbRemoteChanges(changesFlags()); - yield* genKeys({ projectRef: Option.some(LINKED_REF), overrideName: [] }); - expect(calls.map((call) => call.args.slice(-2))).toEqual([ - ["--project-ref", LINKED_REF], - ["--project-ref", LINKED_REF], - ]); - expect(poolerUrls).toEqual([POOLER_URL, POOLER_URL]); - }).pipe(Effect.provide(layer), provideEnv(env)); - }); - - it.effect("forwards no pooler URL when an explicit --project-ref is not the linked ref", () => { - const { calls, poolerUrls, layer } = setup(tmp.current, "stack"); - const explicitRef = "zyxwvutsrqponmlkjihg"; - return Effect.gen(function* () { - yield* writeStackLink(tmp.current, POOLER_URL); - yield* genKeys({ projectRef: Option.some(explicitRef), overrideName: [] }); - expect(calls).toEqual([{ args: ["gen", "keys", "--project-ref", explicitRef] }]); - expect(poolerUrls).toEqual([undefined]); - }).pipe(Effect.provide(layer), provideEnv(env)); - }); -}); diff --git a/apps/cli/src/command-internal/go-proxy-target-env.ts b/apps/cli/src/command-internal/go-proxy-target-env.ts deleted file mode 100644 index a7ee04a76f..0000000000 --- a/apps/cli/src/command-internal/go-proxy-target-env.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Effect, Option } from "effect"; - -import { RemoteTarget } from "../config/remote-target.service.ts"; -import { remoteTargetRuntimeLayer } from "./management-api-runtime.layer.ts"; -import { softProjectStateMode } from "./project-state-mode.ts"; - -const loadTarget = (projectRef: Option.Option, picker: boolean) => - Effect.gen(function* () { - const remoteTarget = yield* RemoteTarget; - const target = yield* picker ? remoteTarget.resolve(projectRef) : remoteTarget.load(projectRef); - yield* Effect.annotateCurrentSpan({ - "remote_target.source": target.source, - "project.ref": target.ref, - }); - return target; - }); - -/** - * The `--project-ref` argument and env for a Go-delegated command that accepts one: stack mode - * resolves the target in TypeScript and passes its ref explicitly, with the linked pooler URL - * only when the ref is the linked one; legacy mode forwards only the flag the user typed and - * never builds the target runtime. With `picker`, a TTY run with nothing linked prompts for a - * project instead of failing. - */ -export const goProjectTarget = Effect.fn("GoProxy.projectTarget")(function* ( - projectRef: Option.Option, - options: { readonly picker?: boolean } = {}, -) { - if ((yield* softProjectStateMode) !== "stack") { - const env: Record = {}; - return { args: Option.isSome(projectRef) ? ["--project-ref", projectRef.value] : [], env }; - } - const target = yield* loadTarget(projectRef, options.picker === true).pipe( - Effect.provide(remoteTargetRuntimeLayer), - ); - const env: Record = {}; - if (target.poolerUrl !== undefined) env["SUPABASE_INTERNAL_POOLER_URL"] = target.poolerUrl; - return { args: ["--project-ref", target.ref], env }; -}); diff --git a/apps/cli/src/command-internal/go-proxy.layer.ts b/apps/cli/src/command-internal/go-proxy.layer.ts deleted file mode 100644 index d9cf7320b5..0000000000 --- a/apps/cli/src/command-internal/go-proxy.layer.ts +++ /dev/null @@ -1,361 +0,0 @@ -import { createRequire } from "node:module"; -import os from "node:os"; -import process from "node:process"; -import { Config, Effect, FileSystem, Layer, Option, Path, Schema, Stream } from "effect"; -import * as ChildProcess from "effect/process/ChildProcess"; -import { ChildProcessSpawner } from "effect/process/ChildProcessSpawner"; -import { resolveWorkdir } from "../config/command-settings.layer.ts"; -import { CLI_VERSION } from "../shared/cli/version.ts"; -import { ProcessControl } from "../shared/runtime/process-control.service.ts"; -import { RuntimeInfo } from "../shared/runtime/runtime-info.service.ts"; -import { GoChildExitError } from "./go-child-exit.error.ts"; -import { GoProxyInvocation } from "./go-proxy-invocation.ts"; -import { GoProxy } from "./go-proxy.service.ts"; -import { readProjectLinkFile } from "./project-link.ts"; -import { usesStackState } from "./stack-backend.ts"; -import { shellEnv } from "./viper-env.ts"; -import { withProcessSpan } from "../shared/telemetry/spans.ts"; - -const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); - -const markDelegated = Effect.serviceOption(GoProxyInvocation).pipe( - Effect.flatMap((invocation) => - Option.isSome(invocation) ? invocation.value.markDelegated : Effect.void, - ), -); - -/** - * Tells the Go child to leave `supabase/.temp` alone when the project keeps its state in - * `$SUPABASE_HOME`, and hands it the linked project's identity as the JSON of its - * `linked-project.json` record so it can still attribute telemetry. Read from the calling fiber, - * like `markDelegated`. - */ -const makeStackStateEnv = ( - fs: FileSystem.FileSystem, - path: Path.Path, - workdir: Effect.Effect, -): Effect.Effect> => - Effect.gen(function* (): Effect.fn.Return> { - if (!(yield* usesStackState)) return {}; - const link = yield* readProjectLinkFile(fs, path, yield* workdir).pipe( - Effect.orElseSucceed(() => Option.none()), - ); - if (Option.isNone(link)) return { SUPABASE_INTERNAL_STACK_STATE: "1" }; - const { project } = link.value; - return { - SUPABASE_INTERNAL_STACK_STATE: "1", - SUPABASE_INTERNAL_LINKED_PROJECT: yield* encodeJson({ - ref: project.ref, - name: project.name ?? "", - organization_id: project.organization_id ?? "", - organization_slug: project.organization_slug ?? "", - }).pipe(Effect.orDie), - }; - }); - -const PLATFORM_CANDIDATES: Partial>>>> = - { - darwin: { arm64: ["darwin-arm64"], x64: ["darwin-x64"] }, - linux: { - arm64: ["linux-arm64", "linux-arm64-musl"], - x64: ["linux-x64", "linux-x64-musl"], - }, - win32: { arm64: ["windows-arm64"], x64: ["windows-x64"] }, - }; - -const require = createRequire(import.meta.url); - -/** - * Outcome of looking up `supabase-go`. `notFound` carries every location checked, so the error can - * be specific about what was tried, and callers never silently fall back to `supabase` on PATH - * (which would resolve to this shim itself and fork-bomb). - */ -export type BinaryResolution = - | { readonly found: string } - | { readonly notFound: ReadonlyArray }; - -const resolveBinary = Effect.fnUntraced(function* ( - fs: FileSystem.FileSystem, - path: Path.Path, -): Effect.fn.Return { - const exists = (candidate: string) => - fs.exists(candidate).pipe(Effect.orElseSucceed(() => false)); - const tried: string[] = []; - - const envBin = yield* shellEnv("SUPABASE_GO_BINARY"); - if (envBin) return { found: envBin }; - tried.push("$SUPABASE_GO_BINARY (unset)"); - - const ext = process.platform === "win32" ? ".exe" : ""; - - // When running as a compiled standalone binary (exec'd by the base shim), process.execPath is - // this binary's own path; look for supabase-go co-located next to it. - const colocated = path.join(path.dirname(process.execPath), `supabase-go${ext}`); - if (yield* exists(colocated)) return { found: colocated }; - tried.push(`${colocated} (not found alongside the shim)`); - - // When running from source, resolve via installed npm packages. - // Guard with exists — in dev the workspace stub packages exist but their bin/ is empty. - const candidates = PLATFORM_CANDIDATES[process.platform]?.[os.arch()] ?? []; - for (const suffix of candidates) { - const pkgJson = yield* Effect.try(() => - require.resolve(`@supabase/cli-${suffix}/package.json`), - ).pipe(Effect.option); - if (Option.isNone(pkgJson)) { - tried.push(`@supabase/cli-${suffix} (npm package not installed)`); - continue; - } - const bin = path.join(path.dirname(pkgJson.value), "bin", `supabase-go${ext}`); - if (yield* exists(bin)) return { found: bin }; - tried.push(`${bin} (npm package present, binary missing)`); - } - - return { notFound: tried }; -}); - -/** - * Builds a `curl | tar` install snippet for the host platform, using the version baked into this - * shim at build time. Returns null when there's no concrete release URL (dev build) or the host - * arch isn't one the release pipeline targets. - */ -function reinstallTarballSnippet(): ReadonlyArray | null { - if (CLI_VERSION === "0.0.0-dev") return null; - const archSuffix = process.arch === "x64" ? "amd64" : process.arch === "arm64" ? "arm64" : null; - if (archSuffix === null) return null; - // Node's `process.platform` is `win32`; GitHub release assets use the modern `windows` slug. - const osSlug = process.platform === "win32" ? "windows" : process.platform; - const asset = `supabase_${CLI_VERSION}_${osSlug}_${archSuffix}.tar.gz`; - return [ - ` mkdir -p "$HOME/.local/share/supabase"`, - ` curl -sL https://github.com/supabase/cli/releases/download/v${CLI_VERSION}/${asset} \\`, - ` | tar -xzf - -C "$HOME/.local/share/supabase"`, - ` export PATH="$HOME/.local/share/supabase:$PATH"`, - ]; -} - -export function formatGoBinaryNotFoundError(tried: ReadonlyArray): string { - const snippet = reinstallTarballSnippet(); - return [ - "Could not find the `supabase-go` binary.", - "", - "The Supabase CLI ships as two co-located binaries: `supabase` (this shim)", - "and `supabase-go` (the Go CLI that the shim forwards to). The shim looked", - "for `supabase-go` in:", - "", - ...tried.map((line) => ` • ${line}`), - "", - "To fix, do one of:", - " • Extract the release tarball into a directory and add the directory to", - " PATH (do not move `supabase` somewhere `supabase-go` doesn't follow).", - ...(snippet === null ? [] : [" For example, on this host:", "", ...snippet, ""]), - " • Install via npm: `npm i -g supabase`.", - " • Set SUPABASE_GO_BINARY to the absolute path of `supabase-go`.", - ].join("\n"); -} - -/** - * Creates a GoProxy layer. - * - * In production use `goProxyLayer` (no options). - * - * In tests pass `{ cwd, env }` to run the binary in an isolated directory - * with a controlled SUPABASE_HOME, so tests don't pollute the real home dir: - * - * makeGoProxyLayer({ - * cwd: projectDir, - * env: { SUPABASE_HOME: homeDir, SUPABASE_NO_KEYRING: "1", SUPABASE_TELEMETRY_DISABLED: "1" }, - * }) - * - * @public - */ -export function makeGoProxyLayer(opts?: { - cwd?: string; - /** - * Extra env for every spawned child. - */ - env?: Record; - globalArgs?: ReadonlyArray; - /** - * The `--workdir` the user passed, which locates the project link handed to stack-mode children. - */ - workdir?: Option.Option; - /** - * Let the parent emit its success tail after re-emitting captured stdout. - */ - parentOwnsCapturedSuccessTail?: boolean; - /** - * Override binary resolution. Primarily a test seam so specs don't have to - * pin `SUPABASE_GO_BINARY` or stub the filesystem: - * - `string` — treat as the resolved Go binary path. - * - `{ notFound: [...] }` — simulate the not-found path; `.exec` will print - * the diagnostic and fail with a non-zero exit code. - * - * In production, leave unset and let `resolveBinary()` pick the right - * artifact for the host platform. - */ - binary?: string | BinaryResolution; -}): Layer.Layer< - GoProxy, - never, - ProcessControl | ChildProcessSpawner | FileSystem.FileSystem | Path.Path | RuntimeInfo -> { - return Layer.effect( - GoProxy, - Effect.gen(function* () { - const processControl = yield* ProcessControl; - const spawner = yield* ChildProcessSpawner; - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const runtimeInfo = yield* RuntimeInfo; - const resolved: BinaryResolution = - typeof opts?.binary === "string" - ? { found: opts.binary } - : (opts?.binary ?? (yield* resolveBinary(fs, path))); - const globalArgs = opts?.globalArgs ?? []; - const projectWorkdir = Effect.gen(function* () { - const envValue = yield* Config.option(Config.String("SUPABASE_WORKDIR")).pipe( - Effect.orElseSucceed(() => Option.none()), - ); - const { workdir } = yield* resolveWorkdir( - opts?.workdir ?? Option.none(), - envValue, - opts?.cwd ?? runtimeInfo.cwd, - (candidate) => fs.exists(candidate).pipe(Effect.orElseSucceed(() => false)), - path, - ); - return workdir; - }); - const stackStateEnv = makeStackStateEnv(fs, path, projectWorkdir); - - return GoProxy.of({ - exec: (args, execOpts) => - Effect.scoped( - Effect.gen(function* () { - if (!("found" in resolved)) { - yield* Effect.sync(() => { - process.stderr.write(`${formatGoBinaryNotFoundError(resolved.notFound)}\n`); - }); - return yield* new GoChildExitError({ - exitCode: 1, - message: "supabase-go binary not found", - }); - } - const binary = resolved.found; - - // Hold terminal signals on the parent for the child's lifetime: the child spawner - // defaults to `detached: true` on non-Windows, which would put the child in its own - // process group and miss tty signals, so `detached: false` below lets Ctrl+C reach - // the Go binary directly. Without a listener, Bun/Node would also default-terminate - // the parent on SIGINT before the child's real exit code is known. - yield* processControl.holdSignals(["SIGINT", "SIGTERM", "SIGHUP"]); - // Only an instrumented caller that delegates the whole command suppresses child - // telemetry: the parent already emits `cli_command_executed` there. Pure proxy - // commands have no parent event, so the child must stay free to report. - const env = { - ...opts?.env, - ...execOpts?.env, - ...(execOpts?.suppressChildTelemetry === true - ? { SUPABASE_TELEMETRY_DISABLED: "1" } - : {}), - ...(yield* stackStateEnv), - }; - const argv = [...globalArgs, ...args]; - const exitCode = yield* withProcessSpan( - "GoProxy.exec", - { executable: binary, argCount: argv.length }, - (traceEnv) => - spawner - .exitCode( - ChildProcess.make(binary, argv, { - cwd: execOpts?.cwd ?? opts?.cwd, - env: { ...env, ...traceEnv }, - extendEnv: true, - stdin: "inherit", - stdout: "inherit", - stderr: "inherit", - detached: false, - }), - ) - .pipe(Effect.orDie), - ); - if (exitCode !== 0) { - return yield* new GoChildExitError({ - exitCode, - message: `supabase-go exited with code ${exitCode} (see stderr for details)`, - }); - } - yield* markDelegated; - }), - ), - execCapture: (args, execOpts) => - Effect.scoped( - Effect.gen(function* () { - if (!("found" in resolved)) { - yield* Effect.sync(() => { - process.stderr.write(`${formatGoBinaryNotFoundError(resolved.notFound)}\n`); - }); - return yield* new GoChildExitError({ - exitCode: 1, - message: "supabase-go binary not found", - }); - } - const binary = resolved.found; - yield* processControl.holdSignals(["SIGINT", "SIGTERM", "SIGHUP"]); - // Same rule as `exec`: only an instrumented caller that owns the - // parent `cli_command_executed` event suppresses child telemetry. - const env = { - ...opts?.env, - ...execOpts?.env, - ...(execOpts?.suppressChildTelemetry === true - ? { SUPABASE_TELEMETRY_DISABLED: "1" } - : {}), - ...(opts?.parentOwnsCapturedSuccessTail === true - ? { SUPABASE_NO_UPDATE_NOTIFIER: "1" } - : {}), - ...(yield* stackStateEnv), - }; - // Capture stdout while keeping stderr inherited, so progress still reaches the user - // while stdout is collected for wrapping. Callers pass stdin: "ignore" to give the - // child a non-TTY stdin so it can't block on a prompt before the wrapper emits its - // machine-output envelope. - const argv = [...globalArgs, ...args]; - const { captured, exitCode } = yield* withProcessSpan( - "GoProxy.execCapture", - { executable: binary, argCount: argv.length }, - (traceEnv) => - Effect.gen(function* () { - const handle = yield* spawner.spawn( - ChildProcess.make(binary, argv, { - cwd: execOpts?.cwd ?? opts?.cwd, - env: { ...env, ...traceEnv }, - extendEnv: true, - stdin: execOpts?.stdin ?? "inherit", - stdout: "pipe", - stderr: "inherit", - detached: false, - }), - ); - // Drain stdout fully before awaiting exit so a full pipe buffer can't - // deadlock the child. - const captured = yield* Stream.mkString(Stream.decodeText(handle.stdout)); - const exitCode = yield* handle.exitCode; - return { captured, exitCode }; - }).pipe(Effect.orDie), - (result) => result.exitCode, - ); - if (exitCode !== 0) { - return yield* new GoChildExitError({ - exitCode, - message: `supabase-go exited with code ${exitCode} (see stderr for details)`, - }); - } - if (opts?.parentOwnsCapturedSuccessTail !== true) { - yield* markDelegated; - } - return captured; - }), - ), - }); - }), - ); -} diff --git a/apps/cli/src/command-internal/go-proxy.layer.unit.test.ts b/apps/cli/src/command-internal/go-proxy.layer.unit.test.ts deleted file mode 100644 index a4bcfce155..0000000000 --- a/apps/cli/src/command-internal/go-proxy.layer.unit.test.ts +++ /dev/null @@ -1,685 +0,0 @@ -import { describe, expect, it, vi } from "@effect/vitest"; -import { BunServices } from "@effect/platform-bun"; -import { - Cause, - Deferred, - Effect, - Exit, - Fiber, - FileSystem, - Layer, - Path, - PlatformError, - Schema, - Sink, - Stream, -} from "effect"; -import { ChildProcessSpawner } from "effect/process"; -import { - type CliProcessSignal, - ProcessControl, -} from "../shared/runtime/process-control.service.ts"; -import { mockRuntimeInfo } from "../../tests/helpers/mocks.ts"; -import { useTempWorkdir } from "../../tests/helpers/command-mocks.ts"; -import { GoChildExitError } from "./go-child-exit.error.ts"; -import { GoProxyInvocation, goProxyInvocationLayer } from "./go-proxy-invocation.ts"; -import { GoProxy } from "./go-proxy.service.ts"; -import { projectStateModeLayer } from "./project-state-mode.ts"; -import { stackBackendLayer } from "./stack-backend.ts"; -import { formatGoBinaryNotFoundError, makeGoProxyLayer } from "./go-proxy.layer.ts"; - -/** - * Regression tests for SIGINT propagation: Ctrl+C on a proxied long-running command must reach - * the Go sidecar and not lose its exit code. `ChildProcess.make` must be called with - * `detached: false`, and `processControl.holdSignals` must be acquired before spawn (covering - * SIGINT/SIGTERM/SIGHUP) and released on every exit path. - */ - -type CapturedCommand = { - command: string; - args: readonly string[]; - options: { - detached?: boolean; - stdin?: unknown; - stdout?: unknown; - stderr?: unknown; - cwd?: string; - env?: Record; - extendEnv?: boolean; - }; -}; - -type ExitBehavior = - | { kind: "success"; code: number } - | { kind: "never" } - | { kind: "fail"; error: string }; - -type HoldEvent = - | { kind: "acquire"; id: number; signals: ReadonlyArray } - | { kind: "release"; id: number }; - -/** - * Records holdSignals(...) acquire/release transitions. Each acquire gets a monotonically - * increasing id so tests can pair an acquire with its release. - * - * `exit()` here only guards against a regression that reintroduces a direct - * `ProcessControl.exit()` call; it blocks on `Effect.never` since nothing in this file exercises - * it. - */ -function mockProcessControl() { - const holdEvents: HoldEvent[] = []; - const exitCalls: number[] = []; - let nextHoldId = 0; - - const exit = (code: number) => - Effect.sync(() => { - exitCalls.push(code); - }).pipe(Effect.flatMap(() => Effect.never)); - - return { - get holdEvents() { - return holdEvents; - }, - get exitCalls() { - return exitCalls; - }, - layer: Layer.succeed( - ProcessControl, - ProcessControl.of({ - awaitSignal: () => Effect.never, - awaitShutdown: Effect.never, - holdSignals: (signals) => - Effect.acquireRelease( - Effect.sync(() => { - const id = nextHoldId++; - holdEvents.push({ kind: "acquire", id, signals }); - return id; - }), - (id) => - Effect.sync(() => { - holdEvents.push({ kind: "release", id }); - }), - ).pipe(Effect.asVoid), - exit, - setExitCode: () => Effect.void, - getExitCode: Effect.undefined, - }), - ), - }; -} - -/** - * Builds a mock `ChildProcessSpawner` that records every spawned command and returns a - * controllable exit code. `spawnedBeforeExit` resolves as soon as the spawn is observed, to - * sequence a race-then-interrupt. - */ -function mockSpawner(exit: ExitBehavior, spawnedBeforeExit?: Deferred.Deferred) { - const spawned: CapturedCommand[] = []; - const layer = Layer.succeed( - ChildProcessSpawner.ChildProcessSpawner, - ChildProcessSpawner.make((command: any) => - Effect.sync(() => { - const cmd = command as CapturedCommand & { _tag: string }; - spawned.push({ - command: cmd.command, - args: cmd.args, - options: cmd.options, - }); - if (spawnedBeforeExit !== undefined) { - Deferred.doneUnsafe(spawnedBeforeExit, Effect.void); - } - const exitCode = - exit.kind === "success" - ? Effect.succeed(ChildProcessSpawner.ExitCode(exit.code)) - : exit.kind === "never" - ? Effect.never - : Effect.fail( - PlatformError.systemError({ - _tag: "Unknown", - module: "ChildProcess", - method: "exitCode", - description: exit.error, - }), - ); - return ChildProcessSpawner.makeHandle({ - pid: ChildProcessSpawner.ProcessId(42_424), - exitCode, - isRunning: Effect.succeed(false), - kill: () => Effect.void, - unref: Effect.succeed(Effect.void), - stdin: Sink.drain as any, - stdout: Stream.empty, - stderr: Stream.empty, - all: Stream.empty, - getInputFd: () => Sink.drain as any, - getOutputFd: () => Stream.empty, - }); - }), - ), - ); - return { layer, spawned }; -} - -/** Injected directly via `makeGoProxyLayer({ binary })` so tests don't depend on workspace package state or `process.env`. */ -const TEST_BINARY = "/test/fake-supabase-go"; - -describe("formatGoBinaryNotFoundError", () => { - const TRIED = [ - "$SUPABASE_GO_BINARY (unset)", - "/usr/local/bin/supabase-go (not found alongside the shim)", - "@supabase/cli-linux-x64 (npm package not installed)", - ]; - - it("renders each tried location as a bullet and includes remediation hints", () => { - const message = formatGoBinaryNotFoundError(TRIED); - expect(message).toContain("Could not find the `supabase-go` binary"); - expect(message).toContain(" • $SUPABASE_GO_BINARY (unset)"); - expect(message).toContain(" • /usr/local/bin/supabase-go (not found alongside the shim)"); - expect(message).toContain(" • @supabase/cli-linux-x64 (npm package not installed)"); - expect(message).toContain("npm i -g supabase"); - expect(message).toContain("SUPABASE_GO_BINARY"); - }); - - it("omits the curl|tar snippet on dev builds (no CLI_VERSION baked in)", () => { - const message = formatGoBinaryNotFoundError(TRIED); - expect(message).not.toContain("curl -sL"); - expect(message).toContain("Extract the release tarball"); - }); -}); - -// Instantiates a fresh module with a stubbed CLI_VERSION to assert against a known release -// version + asset filename; nested in its own describe so the module mock doesn't bleed into -// other suites. -describe("formatGoBinaryNotFoundError - pinned snippet", () => { - const TRIED = ["$SUPABASE_GO_BINARY (unset)"]; - const PINNED_VERSION = "2.100.0"; - - function withMockedHost( - opts: { platform: NodeJS.Platform; arch: NodeJS.Architecture }, - fn: (mod: typeof import("./go-proxy.layer.ts")) => void, - ): Effect.Effect { - return Effect.acquireUseRelease( - Effect.sync(() => { - vi.resetModules(); - vi.doMock("../shared/cli/version.ts", () => ({ CLI_VERSION: PINNED_VERSION })); - const originalPlatform = process.platform; - const originalArch = process.arch; - Object.defineProperty(process, "platform", { value: opts.platform, configurable: true }); - Object.defineProperty(process, "arch", { value: opts.arch, configurable: true }); - return { originalPlatform, originalArch }; - }), - () => Effect.promise(() => import("./go-proxy.layer.ts")).pipe(Effect.map(fn)), - ({ originalPlatform, originalArch }) => - Effect.sync(() => { - Object.defineProperty(process, "platform", { - value: originalPlatform, - configurable: true, - }); - Object.defineProperty(process, "arch", { value: originalArch, configurable: true }); - vi.doUnmock("../cli/version.ts"); - vi.resetModules(); - }), - ); - } - - it.effect("renders a copy-pasteable install snippet for linux x64", () => - withMockedHost({ platform: "linux", arch: "x64" }, (mod) => { - const message = mod.formatGoBinaryNotFoundError(TRIED); - expect(message).toContain( - `https://github.com/supabase/cli/releases/download/v${PINNED_VERSION}/supabase_${PINNED_VERSION}_linux_amd64.tar.gz`, - ); - expect(message).toContain(`mkdir -p "$HOME/.local/share/supabase"`); - expect(message).toContain(`export PATH="$HOME/.local/share/supabase:$PATH"`); - }), - ); - - it.effect("maps Node's win32 platform to the release asset's `windows` slug", () => - withMockedHost({ platform: "win32", arch: "x64" }, (mod) => { - const message = mod.formatGoBinaryNotFoundError(TRIED); - expect(message).toContain( - `https://github.com/supabase/cli/releases/download/v${PINNED_VERSION}/supabase_${PINNED_VERSION}_windows_amd64.tar.gz`, - ); - expect(message).not.toContain("win32"); - }), - ); - - it.effect("maps darwin arm64 to the matching release asset", () => - withMockedHost({ platform: "darwin", arch: "arm64" }, (mod) => { - expect(mod.formatGoBinaryNotFoundError(TRIED)).toContain( - `supabase_${PINNED_VERSION}_darwin_arm64.tar.gz`, - ); - }), - ); - - it.effect("omits the snippet on unsupported architectures (no release asset)", () => - withMockedHost({ platform: "linux", arch: "ia32" }, (mod) => { - expect(mod.formatGoBinaryNotFoundError(TRIED)).not.toContain("curl -sL"); - }), - ); -}); - -const proxyDeps = ( - spawner: Layer.Layer, - processControl: Layer.Layer, -) => Layer.mergeAll(BunServices.layer, mockRuntimeInfo(), spawner, processControl); - -describe("makeGoProxyLayer", () => { - const tmp = useTempWorkdir("supabase-go-proxy-"); - - it.effect("records delegated execution for the parent success trailer", () => { - const spawner = mockSpawner({ kind: "success", code: 0 }); - const pc = mockProcessControl(); - const layer = Layer.mergeAll( - makeGoProxyLayer({ binary: TEST_BINARY }).pipe( - Layer.provide(proxyDeps(spawner.layer, pc.layer)), - ), - goProxyInvocationLayer, - ); - return Effect.gen(function* () { - const proxy = yield* GoProxy; - const invocation = yield* GoProxyInvocation; - - expect(yield* invocation.wasDelegated).toBe(false); - yield* proxy.exec(["migration", "squash", "--local"]); - expect(yield* invocation.wasDelegated).toBe(true); - }).pipe(Effect.provide(layer)); - }); - - it.effect("leaves captured success tails to the parent when configured", () => { - const spawner = mockSpawner({ kind: "success", code: 0 }); - const pc = mockProcessControl(); - const layer = Layer.mergeAll( - makeGoProxyLayer({ - binary: TEST_BINARY, - env: { SUPABASE_NO_UPDATE_NOTIFIER: "0" }, - parentOwnsCapturedSuccessTail: true, - }).pipe(Layer.provide(proxyDeps(spawner.layer, pc.layer))), - goProxyInvocationLayer, - ); - return Effect.gen(function* () { - const proxy = yield* GoProxy; - const invocation = yield* GoProxyInvocation; - - yield* proxy.execCapture(["db", "diff"], { - env: { SUPABASE_NO_UPDATE_NOTIFIER: "0" }, - }); - - expect(spawner.spawned[0]?.options.env?.SUPABASE_NO_UPDATE_NOTIFIER).toBe("1"); - expect(yield* invocation.wasDelegated).toBe(false); - }).pipe(Effect.provide(layer)); - }); - - it.effect("passes detached:false and inherited stdio to the spawner", () => { - const spawner = mockSpawner({ kind: "success", code: 0 }); - const pc = mockProcessControl(); - const layer = makeGoProxyLayer({ binary: TEST_BINARY, globalArgs: ["--debug"] }).pipe( - Layer.provide(proxyDeps(spawner.layer, pc.layer)), - ); - return Effect.gen(function* () { - const proxy = yield* GoProxy; - yield* proxy.exec(["projects", "list"]); - - expect(spawner.spawned).toHaveLength(1); - const captured = spawner.spawned[0]!; - expect(captured.command).toBe(TEST_BINARY); - expect(captured.args).toEqual(["--debug", "projects", "list"]); - expect(captured.options.detached).toBe(false); - expect(captured.options.stdin).toBe("inherit"); - expect(captured.options.stdout).toBe("inherit"); - expect(captured.options.stderr).toBe("inherit"); - expect(captured.options.extendEnv).toBe(true); - }).pipe(Effect.provide(layer)); - }); - - it.effect("leaves child telemetry enabled for pure proxy commands", () => { - const spawner = mockSpawner({ kind: "success", code: 0 }); - const pc = mockProcessControl(); - const layer = makeGoProxyLayer({ binary: TEST_BINARY }).pipe( - Layer.provide(proxyDeps(spawner.layer, pc.layer)), - ); - return Effect.gen(function* () { - const proxy = yield* GoProxy; - yield* proxy.exec(["migration", "squash"]); - yield* proxy.execCapture(["gen", "keys"]); - - for (const captured of spawner.spawned) { - expect(captured.options.env ?? {}).not.toHaveProperty("SUPABASE_TELEMETRY_DISABLED"); - } - }).pipe(Effect.provide(layer)); - }); - - it.effect("suppresses child telemetry when the caller owns the parent event", () => { - const spawner = mockSpawner({ kind: "success", code: 0 }); - const pc = mockProcessControl(); - const layer = makeGoProxyLayer({ binary: TEST_BINARY }).pipe( - Layer.provide(proxyDeps(spawner.layer, pc.layer)), - ); - return Effect.gen(function* () { - const proxy = yield* GoProxy; - yield* proxy.exec(["db", "pull"], { suppressChildTelemetry: true }); - yield* proxy.execCapture(["db", "diff"], { - env: { CUSTOM: "kept" }, - suppressChildTelemetry: true, - }); - - for (const captured of spawner.spawned) { - expect(captured.options.env).toMatchObject({ SUPABASE_TELEMETRY_DISABLED: "1" }); - } - expect(spawner.spawned[1]?.options.env).toMatchObject({ CUSTOM: "kept" }); - }).pipe(Effect.provide(layer)); - }); - - it.effect( - "tells children to leave supabase/.temp alone and who is linked in stack mode only", - () => { - const spawner = mockSpawner({ kind: "success", code: 0 }); - const pc = mockProcessControl(); - const layer = makeGoProxyLayer({ binary: TEST_BINARY, cwd: tmp.current }).pipe( - Layer.provide(proxyDeps(spawner.layer, pc.layer)), - ); - const run = (modeLayer: Layer.Layer | undefined) => - Effect.gen(function* () { - const proxy = yield* GoProxy; - yield* proxy.exec(["db", "remote", "changes"]); - yield* proxy.execCapture(["db", "diff"]); - }).pipe(Effect.provide(modeLayer === undefined ? layer : Layer.merge(layer, modeLayer))); - const childEnv = () => - spawner.spawned.splice(0).map((captured) => ({ - stackState: captured.options.env?.SUPABASE_INTERNAL_STACK_STATE, - linked: captured.options.env?.SUPABASE_INTERNAL_LINKED_PROJECT, - pooler: captured.options.env?.SUPABASE_INTERNAL_POOLER_URL, - })); - - return Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const unlinked = { stackState: "1", linked: undefined, pooler: undefined }; - const absent = { stackState: undefined, linked: undefined, pooler: undefined }; - - yield* run(projectStateModeLayer("stack")); - expect(childEnv()).toEqual([unlinked, unlinked]); - - yield* fs.makeDirectory(path.join(tmp.current, "supabase"), { recursive: true }); - const writeLink = (poolerUrl?: string) => - fs.writeFileString( - path.join(tmp.current, "supabase", ".link.json"), - JSON.stringify({ - project: { - ref: "abcdefghijklmnopqrst", - name: "Acme", - organization_id: "org_123", - organization_slug: "acme", - }, - active_branch: { - ref: "tsrqponmlkjihgfedcba", - name: "feature-x", - is_default: false, - ...(poolerUrl === undefined ? {} : { pooler_url: poolerUrl }), - }, - fetchedAt: "2026-01-01T00:00:00.000Z", - }), - ); - const linked = { - stackState: "1", - linked: yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({ - ref: "abcdefghijklmnopqrst", - name: "Acme", - organization_id: "org_123", - organization_slug: "acme", - }), - pooler: undefined, - }; - yield* writeLink(); - yield* run(projectStateModeLayer("stack")); - yield* run(stackBackendLayer("stack")); - expect(childEnv()).toEqual([linked, linked, linked, linked]); - - const poolerUrl = - "postgresql://postgres.tsrqponmlkjihgfedcba:[YOUR-PASSWORD]@pooler.example.com:6543/postgres"; - yield* writeLink(poolerUrl); - yield* run(projectStateModeLayer("stack")); - expect(childEnv()).toEqual([linked, linked]); - - yield* run(projectStateModeLayer("legacy")); - yield* run(undefined); - expect(childEnv()).toEqual([absent, absent, absent, absent]); - }).pipe(Effect.provide(BunServices.layer)); - }, - ); - - it.effect("passes the layer's env to children, letting per-call env win", () => { - const spawner = mockSpawner({ kind: "success", code: 0 }); - const pc = mockProcessControl(); - const layer = makeGoProxyLayer({ - binary: TEST_BINARY, - env: { SUPABASE_NO_UPDATE_NOTIFIER: "1" }, - }).pipe(Layer.provide(proxyDeps(spawner.layer, pc.layer))); - return Effect.gen(function* () { - const proxy = yield* GoProxy; - yield* proxy.exec(["projects", "list"]); - yield* proxy.execCapture(["gen", "keys"]); - yield* proxy.exec(["projects", "list"], { env: { SUPABASE_NO_UPDATE_NOTIFIER: "0" } }); - - expect(spawner.spawned[0]?.options.env?.SUPABASE_NO_UPDATE_NOTIFIER).toBe("1"); - expect(spawner.spawned[1]?.options.env?.SUPABASE_NO_UPDATE_NOTIFIER).toBe("1"); - expect(spawner.spawned[2]?.options.env?.SUPABASE_NO_UPDATE_NOTIFIER).toBe("0"); - }).pipe(Effect.provide(layer)); - }); - - it.effect("propagates non-zero exit codes via GoChildExitError", () => { - const spawner = mockSpawner({ kind: "success", code: 7 }); - const pc = mockProcessControl(); - const layer = makeGoProxyLayer({ binary: TEST_BINARY }).pipe( - Layer.provide(proxyDeps(spawner.layer, pc.layer)), - ); - return Effect.gen(function* () { - const proxy = yield* GoProxy; - const exit = yield* proxy.exec(["some", "command"]).pipe(Effect.exit); - expect(Exit.isFailure(exit)).toBe(true); - if (Exit.isFailure(exit)) { - const error = Cause.squash(exit.cause); - expect(error).toBeInstanceOf(GoChildExitError); - expect((error as GoChildExitError).exitCode).toBe(7); - } - expect(pc.exitCalls).toEqual([]); - }).pipe(Effect.provide(layer)); - }); - - it.effect("lets an Effect.ensuring finalizer run after a non-zero exit (CLI-1879)", () => { - const spawner = mockSpawner({ kind: "success", code: 5 }); - const pc = mockProcessControl(); - const layer = makeGoProxyLayer({ binary: TEST_BINARY }).pipe( - Layer.provide(proxyDeps(spawner.layer, pc.layer)), - ); - let finalizerRan = false; - return Effect.gen(function* () { - const proxy = yield* GoProxy; - yield* proxy.exec(["some", "command"]).pipe( - Effect.ensuring( - Effect.sync(() => { - finalizerRan = true; - }), - ), - Effect.exit, - ); - expect(finalizerRan).toBe(true); - }).pipe(Effect.provide(layer)); - }); - - it.effect("does not call ProcessControl.exit when the Go binary exits zero", () => { - const spawner = mockSpawner({ kind: "success", code: 0 }); - const pc = mockProcessControl(); - const layer = makeGoProxyLayer({ binary: TEST_BINARY }).pipe( - Layer.provide(proxyDeps(spawner.layer, pc.layer)), - ); - return Effect.gen(function* () { - const proxy = yield* GoProxy; - yield* proxy.exec(["some", "command"]); - expect(pc.exitCalls).toEqual([]); - }).pipe(Effect.provide(layer)); - }); - - it.effect("calls holdSignals with SIGINT+SIGTERM+SIGHUP before spawning", () => { - const spawner = mockSpawner({ kind: "success", code: 0 }); - const pc = mockProcessControl(); - const layer = makeGoProxyLayer({ binary: TEST_BINARY }).pipe( - Layer.provide(proxyDeps(spawner.layer, pc.layer)), - ); - return Effect.gen(function* () { - const proxy = yield* GoProxy; - yield* proxy.exec([]); - - const acquires = pc.holdEvents.filter((e) => e.kind === "acquire"); - expect(acquires).toHaveLength(1); - expect(acquires[0]!.signals).toEqual(["SIGINT", "SIGTERM", "SIGHUP"]); - - expect(spawner.spawned).toHaveLength(1); - expect(pc.holdEvents[0]).toEqual(expect.objectContaining({ kind: "acquire" })); - }).pipe(Effect.provide(layer)); - }); - - it.effect("releases the holdSignals scope on successful exec", () => { - const spawner = mockSpawner({ kind: "success", code: 0 }); - const pc = mockProcessControl(); - const layer = makeGoProxyLayer({ binary: TEST_BINARY }).pipe( - Layer.provide(proxyDeps(spawner.layer, pc.layer)), - ); - return Effect.gen(function* () { - const proxy = yield* GoProxy; - yield* proxy.exec([]); - - expect(pc.holdEvents).toEqual([ - { kind: "acquire", id: 0, signals: ["SIGINT", "SIGTERM", "SIGHUP"] }, - { kind: "release", id: 0 }, - ]); - }).pipe(Effect.provide(layer)); - }); - - it.effect("releases the holdSignals scope when the spawner fails", () => { - const spawner = mockSpawner({ kind: "fail", error: "spawn failed" }); - const pc = mockProcessControl(); - const layer = makeGoProxyLayer({ binary: TEST_BINARY }).pipe( - Layer.provide(proxyDeps(spawner.layer, pc.layer)), - ); - return Effect.gen(function* () { - const proxy = yield* GoProxy; - // spawner failures are Effect.orDie'd, so we swallow the defect here. - yield* proxy.exec([]).pipe(Effect.exit); - - expect(pc.holdEvents).toContainEqual({ kind: "release", id: 0 }); - }).pipe(Effect.provide(layer)); - }); - - it.effect("releases the holdSignals scope when the fiber is interrupted", () => { - const spawned = Deferred.makeUnsafe(); - const spawner = mockSpawner({ kind: "never" }, spawned); - const pc = mockProcessControl(); - const layer = makeGoProxyLayer({ binary: TEST_BINARY }).pipe( - Layer.provide(proxyDeps(spawner.layer, pc.layer)), - ); - return Effect.gen(function* () { - const proxy = yield* GoProxy; - const fiber = yield* proxy.exec([]).pipe(Effect.forkChild({ startImmediately: true })); - yield* Deferred.await(spawned); - - expect(pc.holdEvents).toEqual([ - { kind: "acquire", id: 0, signals: ["SIGINT", "SIGTERM", "SIGHUP"] }, - ]); - - yield* Fiber.interrupt(fiber); - - expect(pc.holdEvents).toEqual([ - { kind: "acquire", id: 0, signals: ["SIGINT", "SIGTERM", "SIGHUP"] }, - { kind: "release", id: 0 }, - ]); - }).pipe(Effect.provide(layer)); - }); - - it.effect( - "prints a diagnostic and fails with exit code 1 when supabase-go cannot be resolved", - () => { - const spawner = mockSpawner({ kind: "success", code: 0 }); - const pc = mockProcessControl(); - const stderr = vi.spyOn(process.stderr, "write").mockImplementation(() => true); - const tried = [ - "$SUPABASE_GO_BINARY (unset)", - "/usr/local/bin/supabase-go (not found alongside the shim)", - ]; - const layer = makeGoProxyLayer({ binary: { notFound: tried } }).pipe( - Layer.provide(proxyDeps(spawner.layer, pc.layer)), - ); - return Effect.gen(function* () { - const proxy = yield* GoProxy; - const exit = yield* proxy.exec(["db", "start"]).pipe(Effect.exit); - - expect(spawner.spawned).toHaveLength(0); - expect(Exit.isFailure(exit)).toBe(true); - if (Exit.isFailure(exit)) { - const error = Cause.squash(exit.cause); - expect(error).toBeInstanceOf(GoChildExitError); - expect((error as GoChildExitError).exitCode).toBe(1); - } - expect(pc.exitCalls).toEqual([]); - expect(stderr).toHaveBeenCalledTimes(1); - const written = String(stderr.mock.calls[0]![0]); - expect(written).toContain("Could not find the `supabase-go` binary"); - expect(written).toContain("$SUPABASE_GO_BINARY (unset)"); - expect(written).toContain("/usr/local/bin/supabase-go"); - expect(written).toContain("SUPABASE_GO_BINARY"); - stderr.mockRestore(); - }).pipe(Effect.provide(layer)); - }, - ); - - it.effect( - "execCapture also prints a diagnostic and fails with exit code 1 when supabase-go cannot be resolved", - () => { - const spawner = mockSpawner({ kind: "success", code: 0 }); - const pc = mockProcessControl(); - const stderr = vi.spyOn(process.stderr, "write").mockImplementation(() => true); - const tried = ["$SUPABASE_GO_BINARY (unset)"]; - const layer = makeGoProxyLayer({ binary: { notFound: tried } }).pipe( - Layer.provide(proxyDeps(spawner.layer, pc.layer)), - ); - return Effect.gen(function* () { - const proxy = yield* GoProxy; - const exit = yield* proxy.execCapture(["db", "dump"]).pipe(Effect.exit); - - expect(spawner.spawned).toHaveLength(0); - expect(Exit.isFailure(exit)).toBe(true); - if (Exit.isFailure(exit)) { - const error = Cause.squash(exit.cause); - expect(error).toBeInstanceOf(GoChildExitError); - expect((error as GoChildExitError).exitCode).toBe(1); - } - expect(pc.exitCalls).toEqual([]); - expect(stderr).toHaveBeenCalledTimes(1); - stderr.mockRestore(); - }).pipe(Effect.provide(layer)); - }, - ); - - it.effect("opens and closes a fresh hold scope per sequential exec call", () => { - const spawner = mockSpawner({ kind: "success", code: 0 }); - const pc = mockProcessControl(); - const layer = makeGoProxyLayer({ binary: TEST_BINARY }).pipe( - Layer.provide(proxyDeps(spawner.layer, pc.layer)), - ); - return Effect.gen(function* () { - const proxy = yield* GoProxy; - for (let i = 0; i < 3; i++) { - yield* proxy.exec([`call-${i}`]); - } - - expect(pc.holdEvents).toEqual([ - { kind: "acquire", id: 0, signals: ["SIGINT", "SIGTERM", "SIGHUP"] }, - { kind: "release", id: 0 }, - { kind: "acquire", id: 1, signals: ["SIGINT", "SIGTERM", "SIGHUP"] }, - { kind: "release", id: 1 }, - { kind: "acquire", id: 2, signals: ["SIGINT", "SIGTERM", "SIGHUP"] }, - { kind: "release", id: 2 }, - ]); - expect(spawner.spawned).toHaveLength(3); - }).pipe(Effect.provide(layer)); - }); -}); diff --git a/apps/cli/src/command-internal/go-proxy.service.ts b/apps/cli/src/command-internal/go-proxy.service.ts deleted file mode 100644 index 452fc2c51c..0000000000 --- a/apps/cli/src/command-internal/go-proxy.service.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { Effect } from "effect"; -import { Context } from "effect"; -import type { GoChildExitError } from "./go-child-exit.error.ts"; - -interface GoProxyShape { - /** - * Forwards args to the Go binary, inheriting stdio and propagating the exit code. Fails with - * `GoChildExitError` (carrying the exact exit code) on a non-zero exit or an unresolvable - * binary. - * - * `opts.suppressChildTelemetry` disables telemetry in the child; set it only when the caller's - * own command instrumentation already emits `cli_command_executed`, since a pure proxy - * handler's Go child is its only telemetry emitter. - */ - readonly exec: ( - args: ReadonlyArray, - opts?: { - readonly cwd?: string; - readonly env?: Record; - readonly suppressChildTelemetry?: boolean; - }, - ) => Effect.Effect; - - /** - * Like `exec`, but captures the child's stdout and returns it as a string instead of - * inheriting it; stderr stays inherited. Fails with `GoChildExitError` the same way `exec` - * does. - * - * `opts.stdin: "ignore"` gives the child a non-TTY stdin so a prompt (Go's `PromptYesNo`) - * takes its default instead of blocking — required when a machine-output caller delegates a - * command that would otherwise prompt before the JSON envelope is emitted. - */ - readonly execCapture: ( - args: ReadonlyArray, - opts?: { - readonly cwd?: string; - readonly env?: Record; - readonly stdin?: "inherit" | "ignore"; - readonly suppressChildTelemetry?: boolean; - }, - ) => Effect.Effect; -} - -export class GoProxy extends Context.Service()("supabase/cli/GoProxy") {} diff --git a/apps/cli/src/command-internal/hosted-consent/hosted-consent-coverage.unit.test.ts b/apps/cli/src/command-internal/hosted-consent/hosted-consent-coverage.unit.test.ts new file mode 100644 index 0000000000..6866152680 --- /dev/null +++ b/apps/cli/src/command-internal/hosted-consent/hosted-consent-coverage.unit.test.ts @@ -0,0 +1,133 @@ +import { readFileSync, readdirSync } from "node:fs"; +import { join, sep } from "node:path"; +import { fileURLToPath } from "node:url"; +import { Option } from "effect"; +import { describe, expect, it } from "vitest"; + +import { commandTreeFor } from "../../../tests/helpers/command-flags.ts"; +import type { PermissionVariant } from "../../../tests/helpers/permission-registry.ts"; +import { readTargetPolicy } from "../target/target-policy.ts"; + +const srcDir = fileURLToPath(new URL("../..", import.meta.url)); + +const VARIANTS: ReadonlyArray = [ + undefined, + { stackBackend: "stack", computeEnabled: true }, +]; + +const consentDefaultByLeaf: ReadonlyMap = new Map( + VARIANTS.flatMap((variant) => + [...commandTreeFor(variant)].flatMap(([path, node]) => { + const policy = readTargetPolicy(node.command); + return node.isLeaf && Option.isSome(policy) && policy.value.consent !== "none" + ? [[path, policy.value.consent] as const] + : []; + }), + ), +); + +const consentLeafPaths: ReadonlySet = new Set(consentDefaultByLeaf.keys()); + +const LITERAL_CALL = /\bdescribeHostedConsent\(\s*"([^"]+)"/g; +const ANY_CALL = /\bdescribeHostedConsent\(/g; + +const testFiles = readdirSync(srcDir, { recursive: true, encoding: "utf8" }) + .filter((file) => file.endsWith(".test.ts")) + .map((file) => file.split(sep).join("/")); + +const callsByLeaf = new Map>(); +const nonLiteralCallSites: Array = []; + +for (const file of testFiles) { + const source = readFileSync(join(srcDir, file), "utf8"); + const literalCalls = [...source.matchAll(LITERAL_CALL)]; + for (const [, leaf] of literalCalls) { + callsByLeaf.set(leaf!, [...(callsByLeaf.get(leaf!) ?? []), file]); + } + const anyCalls = [...source.matchAll(ANY_CALL)].length; + if (anyCalls !== literalCalls.length) { + nonLiteralCallSites.push(`${file} (${anyCalls - literalCalls.length})`); + } +} + +describe("hosted consent coverage", () => { + it("scans real test files and finds consenting leaves in the command tree", () => { + expect(testFiles.length).toBeGreaterThan(0); + expect(consentLeafPaths.size).toBeGreaterThan(0); + }); + + it("has a consent matrix for every leaf whose policy asks for consent", () => { + const missing = [...consentLeafPaths].filter((path) => !callsByLeaf.has(path)).sort(); + expect(missing, "leaves with consent but no hosted-consent matrix in src/**/*.test.ts").toEqual( + [], + ); + }); + + it("has no consent matrix for a path that is not a leaf with consent", () => { + const extra = [...callsByLeaf] + .filter(([path]) => !consentLeafPaths.has(path)) + .map(([path, files]) => `${path} (${files.join(", ")})`) + .sort(); + expect(extra, "hosted-consent matrices for paths that are not consenting leaves").toEqual([]); + }); + + it("names the leaf as a string literal at every call site so the scan can see it", () => { + expect(nonLiteralCallSites).toEqual([]); + }); + + it("defaults the prompt to Yes for create-only leaves and No for everything else", () => { + expect(Object.fromEntries(consentDefaultByLeaf)).toMatchInlineSnapshot(` + { + "backups restore": "no", + "bootstrap": "yes", + "branches create": "yes", + "branches delete": "no", + "branches disable": "no", + "branches pause": "no", + "branches unpause": "no", + "branches update": "no", + "compute delete": "no", + "compute push": "no", + "config push": "no", + "db pull": "no", + "db push": "no", + "db query": "no", + "db reset": "no", + "db test": "no", + "domains activate": "no", + "domains create": "yes", + "domains delete": "no", + "encryption update-root-key": "no", + "feedback delete": "no", + "functions delete": "no", + "functions deploy": "no", + "migration down": "no", + "migration repair": "no", + "migration squash": "no", + "migration up": "no", + "network-bans remove": "no", + "network-restrictions update": "no", + "notebooks push": "no", + "orgs create": "yes", + "postgres-config delete": "no", + "postgres-config update": "no", + "projects create": "yes", + "projects delete": "no", + "pull": "no", + "secrets set": "no", + "secrets unset": "no", + "seed buckets": "no", + "ssl-enforcement update": "no", + "sso add": "no", + "sso remove": "no", + "sso update": "no", + "storage cp": "no", + "storage mv": "no", + "storage rm": "no", + "test db": "no", + "vanity-subdomains activate": "no", + "vanity-subdomains delete": "no", + } + `); + }); +}); diff --git a/apps/cli/src/command-internal/hosted-consent/hosted-consent.errors.ts b/apps/cli/src/command-internal/hosted-consent/hosted-consent.errors.ts new file mode 100644 index 0000000000..9b0ad58403 --- /dev/null +++ b/apps/cli/src/command-internal/hosted-consent/hosted-consent.errors.ts @@ -0,0 +1,28 @@ +import { Data } from "effect"; + +import { + actionability, + type CliErrorActionabilityDeclaration, + ErrorActionabilityId, +} from "../../shared/telemetry/error-actionability.ts"; + +/** A hosted mutation needs confirmation and this run can neither prompt nor see `--yes`. */ +export class HostedMutationConsentRequiredError extends Data.TaggedError( + "HostedMutationConsentRequiredError", +)<{ + readonly message: string; + readonly suggestion: string; +}> { + get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { + return actionability.consentRequired; + } +} + +/** The user answered no to a hosted-mutation confirmation. */ +export class HostedMutationDeclinedError extends Data.TaggedError("HostedMutationDeclinedError")<{ + readonly message: string; +}> { + get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { + return actionability.cancelled; + } +} diff --git a/apps/cli/src/command-internal/hosted-consent/hosted-consent.prompt.ts b/apps/cli/src/command-internal/hosted-consent/hosted-consent.prompt.ts new file mode 100644 index 0000000000..91cbaf3097 --- /dev/null +++ b/apps/cli/src/command-internal/hosted-consent/hosted-consent.prompt.ts @@ -0,0 +1,47 @@ +import { Effect, Option } from "effect"; + +import { Output } from "../../shared/output/output.service.ts"; +import { isMachineResourceFormat } from "../../shared/output/resource-format.ts"; +import { AiTool } from "../../shared/telemetry/ai-tool.service.ts"; +import { Tty } from "../../shared/runtime/tty.service.ts"; +import { resolveAgentMode } from "../agent-mode.ts"; +import { AgentFlag, OutputFlag } from "../global-flags.ts"; + +export interface PromptBlocker { + readonly reason: string; + readonly agent: boolean; +} + +const agentDetected = Effect.gen(function* () { + const flag = Option.getOrElse(yield* Effect.serviceOption(AgentFlag), () => "auto" as const); + const tool = yield* Effect.serviceOption(AiTool); + return resolveAgentMode( + flag, + Option.flatMap(tool, (detected) => detected.name), + ); +}); + +/** + * Why this run cannot ask a question, or `None` when it can: text output on an interactive + * stdout with a TTY stdin, no machine `-o` format, and no agent driving it. + */ +export const promptBlocker = Effect.gen(function* () { + const output = yield* Output; + const tty = yield* Tty; + const outputFlag = yield* OutputFlag; + if (yield* agentDetected) { + return Option.some({ reason: "it is running in agent mode", agent: true }); + } + const blocked = (reason: string) => Option.some({ reason, agent: false }); + if (output.format !== "text") return blocked(`its output format is ${output.format}`); + if (isMachineResourceFormat(outputFlag)) { + return blocked( + `it writes machine-readable -o ${Option.getOrElse(outputFlag, () => "")} output`, + ); + } + if (!output.interactive) return blocked("stdout is not a terminal"); + if (!tty.stdinIsTty) return blocked("stdin is not a terminal"); + return Option.none(); +}); + +export const canPrompt = Effect.map(promptBlocker, Option.isNone); diff --git a/apps/cli/src/command-internal/hosted-consent/hosted-consent.token.ts b/apps/cli/src/command-internal/hosted-consent/hosted-consent.token.ts new file mode 100644 index 0000000000..3d4577c6b7 --- /dev/null +++ b/apps/cli/src/command-internal/hosted-consent/hosted-consent.token.ts @@ -0,0 +1,8 @@ +declare const Brand: unique symbol; + +/** Proof that the user approved a hosted mutation; only the consent module constructs one. */ +export interface HostedMutationConsent { + readonly [Brand]: true; + readonly action: string; + readonly via: "prompt" | "yes"; +} diff --git a/apps/cli/src/command-internal/hosted-consent/hosted-consent.ts b/apps/cli/src/command-internal/hosted-consent/hosted-consent.ts new file mode 100644 index 0000000000..c017622f3b --- /dev/null +++ b/apps/cli/src/command-internal/hosted-consent/hosted-consent.ts @@ -0,0 +1,144 @@ +import { Effect, Option } from "effect"; + +import { CommandTarget } from "../../shared/output/command-target.service.ts"; +import { CONTEXT_CANCELED_MESSAGE } from "../../shared/output/errors.ts"; +import { Output } from "../../shared/output/output.service.ts"; +import type { ResolvedDbConfig } from "../db-config.types.ts"; +import type { ResolvedTarget } from "../target/resolve-target.ts"; +import { TargetPolicy } from "../target/target-policy.ts"; +import { + HostedMutationConsentRequiredError, + HostedMutationDeclinedError, +} from "./hosted-consent.errors.ts"; +import { promptBlocker } from "./hosted-consent.prompt.ts"; +import type { PromptBlocker } from "./hosted-consent.prompt.ts"; +import type { HostedMutationConsent } from "./hosted-consent.token.ts"; +import { ignoredYesFiles, resolveHostedYes } from "./hosted-consent.yes.ts"; + +export interface HostedMutationRequest { + /** An imperative, sentence-case phrase with no trailing period, such as "Apply 3 migrations". */ + readonly action: string; + /** Plan lines printed before the question. */ + readonly details?: ReadonlyArray; + /** Asks for `expected` to be typed instead of a yes/no answer. */ + readonly confirm?: { readonly kind: "typed"; readonly expected: string }; + /** The command has a `--read-only` mode that needs no confirmation. */ + readonly readOnlyAlternative?: boolean; +} + +// The brand is type-only, so minting a token needs this one assertion. +const grant = (action: string, via: HostedMutationConsent["via"]): HostedMutationConsent => + ({ action, via }) as HostedMutationConsent; + +/** The first announced target's banner text, or `None` before any announcement. */ +export const announcedTarget = Effect.serviceOption(CommandTarget).pipe( + Effect.flatMap( + Option.match({ + onNone: () => Effect.succeed(Option.none()), + onSome: (cell) => cell.text, + }), + ), +); + +const consentRequired = Effect.fnUntraced(function* ( + request: HostedMutationRequest, + target: Option.Option, + blocker: PromptBlocker, +) { + const ignored = yield* ignoredYesFiles; + const suggestions = [ + blocker.agent + ? "This is an agent run: ask the user before re-running with --yes." + : "Re-run with --yes to confirm.", + ...(request.readOnlyAlternative === true + ? ["For a read-only statement, pass --read-only instead."] + : []), + ...(ignored.length > 0 + ? [`SUPABASE_YES in ${ignored.join(", ")} does not confirm hosted changes; pass --yes.`] + : []), + ]; + const where = Option.match(target, { onNone: () => "", onSome: (text) => ` (target: ${text})` }); + return yield* new HostedMutationConsentRequiredError({ + message: `Confirmation required: ${request.action}${where}. This run can't ask because ${blocker.reason}. Nothing was changed.`, + suggestion: suggestions.join(" "), + }); +}); + +const ask = Effect.fnUntraced(function* ( + request: HostedMutationRequest, + defaultValue: boolean, + onTarget: string, +) { + const output = yield* Output; + if (request.details !== undefined && request.details.length > 0) { + yield* output.raw(`${request.details.join("\n")}\n`, "stderr"); + } + if (request.confirm === undefined) { + return yield* output.promptConfirm(`${request.action}${onTarget}?`, { defaultValue }); + } + yield* output.raw(`${request.action}${onTarget}\n`, "stderr"); + const typed = yield* output.promptText(`Type ${request.confirm.expected} to confirm`); + return typed.trim() === request.confirm.expected; +}); + +/** + * Grants consent for a hosted mutation or fails. `--yes` or a shell `SUPABASE_YES` (never a dotenv + * file) grants it; otherwise only an interactive text-mode terminal with no agent driving it is + * asked. Stdin is never read, so a piped answer does not count. + */ +export const requireHostedMutationConsent = Effect.fn("HostedConsent.require")(function* ( + request: HostedMutationRequest, +) { + const policy = yield* TargetPolicy; + if (policy.consent === "none") { + return yield* Effect.die( + new Error( + `requireHostedMutationConsent needs a target policy with consent "yes" or "no"; "${policy.group}" declares "none"`, + ), + ); + } + const output = yield* Output; + const target = yield* announcedTarget; + const onTarget = Option.match(target, { onNone: () => "", onSome: (text) => ` on ${text}` }); + + const approvedBy = yield* resolveHostedYes; + if (Option.isSome(approvedBy)) { + yield* output.raw(`Confirmed by ${approvedBy.value}: ${request.action}${onTarget}\n`, "stderr"); + yield* Effect.annotateCurrentSpan("consent.via", "yes"); + return grant(request.action, "yes"); + } + + const blocker = yield* promptBlocker; + if (Option.isSome(blocker)) return yield* consentRequired(request, target, blocker.value); + + const confirmed = yield* ask(request, policy.consent === "yes", onTarget).pipe( + Effect.catchTag("NonInteractiveError", () => + consentRequired(request, target, { reason: "no prompt is available", agent: false }), + ), + ); + if (!confirmed) { + return yield* new HostedMutationDeclinedError({ message: CONTEXT_CANCELED_MESSAGE }); + } + yield* Effect.annotateCurrentSpan("consent.via", "prompt"); + return grant(request.action, "prompt"); +}); + +/** `None` for a local target or connection; otherwise {@link requireHostedMutationConsent}. */ +export const requireTargetMutationConsent = Effect.fn("HostedConsent.requireForTarget")(function* ( + target: ResolvedTarget | ResolvedDbConfig, + request: HostedMutationRequest, +) { + const isLocal = "isLocal" in target ? target.isLocal : target.kind === "local"; + if (isLocal) return Option.none(); + return Option.some(yield* requireHostedMutationConsent(request)); +}); + +/** + * `Some` only under `--yes` or a shell `SUPABASE_YES`; never prompts, so a read can hand it to a + * connection that may clear a network ban. + */ +export const hostedMutationPreapproval = Effect.map(resolveHostedYes, (approvedBy) => + Option.isSome(approvedBy) + ? Option.some(grant("Remove network bans", "yes")) + : Option.none(), +); diff --git a/apps/cli/src/command-internal/hosted-consent/hosted-consent.unit.test.ts b/apps/cli/src/command-internal/hosted-consent/hosted-consent.unit.test.ts new file mode 100644 index 0000000000..2926bfe3c2 --- /dev/null +++ b/apps/cli/src/command-internal/hosted-consent/hosted-consent.unit.test.ts @@ -0,0 +1,672 @@ +import { BunServices } from "@effect/platform-bun"; +import { describe, expect, it } from "@effect/vitest"; +import { Cause, Effect, Exit, FileSystem, Layer, Option, Path, Redacted } from "effect"; + +import { + mockCommandSettings, + withConfigEnv, + withEmptyConfigEnv, +} from "../../../tests/helpers/command-mocks.ts"; +import { mockOutput, mockRuntimeInfo, mockTty } from "../../../tests/helpers/mocks.ts"; +import { CliArgs } from "../../shared/cli/cli-args.service.ts"; +import { commandTargetLayer } from "../../shared/output/command-target.layer.ts"; +import { + announceTarget, + type CommandTargetJson, +} from "../../shared/output/command-target.service.ts"; +import type { OutputFormat } from "../../shared/output/types.ts"; +import { AiTool } from "../../shared/telemetry/ai-tool.service.ts"; +import { classifyCliErrorActionability } from "../../shared/telemetry/error-actionability.ts"; +import type { ResolvedDbConfig } from "../db-config.types.ts"; +import { AgentFlag, OutputFlag, YesFlag } from "../global-flags.ts"; +import type { ResolvedTarget } from "../target/resolve-target.ts"; +import { TargetPolicy } from "../target/target-policy.ts"; +import type { TargetPolicyShape } from "../target/target-policy.ts"; +import { + HostedMutationConsentRequiredError, + HostedMutationDeclinedError, +} from "./hosted-consent.errors.ts"; +import { + hostedMutationPreapproval, + requireHostedMutationConsent, + requireTargetMutationConsent, +} from "./hosted-consent.ts"; +import type { HostedMutationRequest } from "./hosted-consent.ts"; + +type OutputFlagValue = "pretty" | "json" | "yaml" | "toml" | "env" | "table" | "csv"; + +interface Scenario { + readonly policy?: TargetPolicyShape; + readonly format?: OutputFormat; + readonly interactive?: boolean; + readonly stdinIsTty?: boolean; + readonly yes?: boolean; + readonly args?: ReadonlyArray; + readonly outputFlag?: OutputFlagValue; + readonly confirmResponses?: ReadonlyArray; + readonly textResponses?: ReadonlyArray; + readonly promptFails?: boolean; + readonly shellYes?: string; + readonly env?: Readonly>; + readonly announced?: string; + readonly agent?: "auto" | "yes" | "no"; + readonly aiTool?: string; + readonly cwd?: string; + readonly workdir?: string; + readonly withoutFilesystem?: boolean; +} + +const REQUEST: HostedMutationRequest = { + action: "Apply 3 migrations", + details: [" 20240101_a.sql", " 20240102_b.sql"], +}; + +const UNVERIFIED_JSON: CommandTargetJson = { + kind: "linked", + project_ref: "abcdefghijklmnopqrst", + environment: "unverified", +}; + +const interactiveTerminal: Scenario = { format: "text", interactive: true, stdinIsTty: true }; + +const harness = (scenario: Scenario) => { + const out = mockOutput({ + format: scenario.format ?? "text", + interactive: scenario.interactive, + promptConfirmResponses: scenario.confirmResponses, + promptTextResponses: scenario.textResponses, + promptConfirmFail: scenario.promptFails, + }); + const layer = Layer.mergeAll( + out.layer, + commandTargetLayer.pipe(Layer.provide(out.layer)), + mockTty({ + stdinIsTty: scenario.stdinIsTty ?? false, + stdoutIsTty: scenario.stdinIsTty ?? false, + }), + Layer.succeed(YesFlag, scenario.yes ?? false), + Layer.succeed(CliArgs, { args: scenario.args ?? [] }), + Layer.succeed( + OutputFlag, + scenario.outputFlag === undefined ? Option.none() : Option.some(scenario.outputFlag), + ), + Layer.succeed(TargetPolicy, scenario.policy ?? { group: "hosted", consent: "no" }), + scenario.agent === undefined ? Layer.empty : Layer.succeed(AgentFlag, scenario.agent), + scenario.aiTool === undefined + ? Layer.empty + : Layer.succeed(AiTool, { name: Option.some(scenario.aiTool) }), + scenario.withoutFilesystem === true + ? Layer.empty + : Layer.merge(BunServices.layer, mockRuntimeInfo({ cwd: scenario.cwd ?? "/test/project" })), + scenario.workdir === undefined + ? Layer.empty + : mockCommandSettings({ workdir: scenario.workdir }), + ); + const shellEnv = { + NODE_ENV: "", + ...(scenario.shellYes === undefined ? {} : { SUPABASE_YES: scenario.shellYes }), + ...scenario.env, + }; + const withShellEnv = (body: Effect.Effect) => + withEmptyConfigEnv(withConfigEnv(shellEnv, body)); + const prepare = (body: Effect.Effect) => + withShellEnv( + (scenario.announced === undefined + ? body + : Effect.flatMap( + announceTarget({ text: scenario.announced, json: UNVERIFIED_JSON }), + () => body, + ) + ).pipe(Effect.provide(layer)), + ); + return { out, prepare }; +}; + +const consent = (scenario: Scenario, request: HostedMutationRequest = REQUEST) => { + const { out, prepare } = harness(scenario); + const exit = prepare(requireHostedMutationConsent(request)).pipe(Effect.exit); + return { out, exit }; +}; + +const successOf = (exit: Exit.Exit): A => { + if (!Exit.isSuccess(exit)) throw new Error(`expected success, got ${Cause.pretty(exit.cause)}`); + return exit.value; +}; + +const failureOf = (exit: Exit.Exit): E => { + if (!Exit.isFailure(exit)) throw new Error("expected a failure"); + const failure = Exit.findErrorOption(exit); + if (Option.isNone(failure)) + throw new Error(`expected a typed failure, got ${Cause.pretty(exit.cause)}`); + return failure.value; +}; + +describe("requireHostedMutationConsent", () => { + describe("default answer", () => { + it.effect.each([ + { consent: "no", expected: false }, + { consent: "yes", expected: true }, + ] as const)( + "offers $expected as the default when the policy says $consent", + ({ consent: policyConsent, expected }) => + Effect.gen(function* () { + const run = consent({ + ...interactiveTerminal, + policy: { group: "hosted", consent: policyConsent }, + confirmResponses: [true], + }); + yield* run.exit; + expect(run.out.promptConfirmCalls).toEqual([ + { message: "Apply 3 migrations?", opts: { defaultValue: expected } }, + ]); + }), + ); + + it.effect("dies when the command declares no consent", () => + Effect.gen(function* () { + const exit = yield* consent({ + ...interactiveTerminal, + policy: { group: "hosted", consent: "none" }, + }).exit; + expect(Exit.isFailure(exit) && Cause.hasDies(exit.cause)).toBe(true); + }), + ); + }); + + describe("--yes", () => { + it.effect("grants without asking and echoes the action and target on stderr", () => + Effect.gen(function* () { + const run = consent({ + yes: true, + format: "json", + announced: "project abcdefghijklmnopqrst", + }); + const token = successOf(yield* run.exit); + expect(token).toMatchObject({ action: "Apply 3 migrations", via: "yes" }); + expect(run.out.promptConfirmCalls).toEqual([]); + expect(run.out.stderrText).toContain( + "Confirmed by --yes: Apply 3 migrations on project abcdefghijklmnopqrst\n", + ); + }), + ); + + it.effect("omits the target when nothing was announced", () => + Effect.gen(function* () { + const run = consent({ yes: true }); + yield* run.exit; + expect(run.out.stderrText).toBe("Confirmed by --yes: Apply 3 migrations\n"); + }), + ); + + it.effect("skips a typed confirmation", () => + Effect.gen(function* () { + const run = consent( + { ...interactiveTerminal, yes: true }, + { action: "delete compute api", confirm: { kind: "typed", expected: "api" } }, + ); + const token = successOf(yield* run.exit); + expect(token.via).toBe("yes"); + expect(run.out.promptTextCalls).toEqual([]); + }), + ); + + it.effect("is overridden by an explicit --yes=false even when SUPABASE_YES is set", () => + Effect.gen(function* () { + const exit = yield* consent({ args: ["--yes=false"], shellYes: "1" }).exit; + expect(failureOf(exit)).toBeInstanceOf(HostedMutationConsentRequiredError); + }), + ); + }); + + describe("SUPABASE_YES", () => { + const withDotenvDir = ( + files: Readonly>, + body: (dir: string) => Effect.Effect, + ) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = yield* fs.makeTempDirectoryScoped({ prefix: "supabase-consent-" }); + for (const [name, contents] of Object.entries(files)) { + yield* fs.makeDirectory(path.dirname(path.join(dir, name)), { recursive: true }); + yield* fs.writeFileString(path.join(dir, name), contents); + } + return yield* body(dir); + }).pipe(Effect.scoped, Effect.provide(BunServices.layer)); + + it.effect( + "grants when set in the shell environment and echoes SUPABASE_YES as the source", + () => + Effect.gen(function* () { + const run = consent({ shellYes: "1", format: "json" }); + const token = successOf(yield* run.exit); + expect(token.via).toBe("yes"); + expect(run.out.stderrText).toContain("Confirmed by SUPABASE_YES: Apply 3 migrations"); + }), + ); + + it.effect("does not grant when it is only in the project .env", () => + withDotenvDir({ "supabase/.env": "SUPABASE_YES=1\n" }, (dir) => + Effect.gen(function* () { + const exit = yield* consent({ cwd: dir, workdir: dir }).exit; + expect(failureOf(exit)).toBeInstanceOf(HostedMutationConsentRequiredError); + }), + ), + ); + + it.effect.each([".env", ".env.local", ".env.development", ".env.development.local"])( + "does not grant a shell value that matches %s in the working directory", + (name) => + withDotenvDir({ [name]: "SUPABASE_YES=1\n" }, (cwd) => + Effect.gen(function* () { + const exit = yield* consent({ shellYes: "1", cwd }).exit; + expect(failureOf(exit)).toBeInstanceOf(HostedMutationConsentRequiredError); + }), + ), + ); + + it.effect("checks the dotenv files NODE_ENV selects", () => + withDotenvDir({ ".env.production": "SUPABASE_YES=1\n" }, (cwd) => + Effect.gen(function* () { + const production = yield* consent({ + shellYes: "1", + cwd, + env: { NODE_ENV: "production" }, + }).exit; + expect(failureOf(production)).toBeInstanceOf(HostedMutationConsentRequiredError); + const development = yield* consent({ shellYes: "1", cwd }).exit; + expect(successOf(development).via).toBe("yes"); + }), + ), + ); + + it.effect.each([ + { name: "a different value", contents: "SUPABASE_YES=true\n" }, + { name: "an inline comment Bun strips", contents: "SUPABASE_YES=1#x\n" }, + { name: "an empty value", contents: "SUPABASE_YES=\n" }, + ])( + "does not grant a shell value when the working directory's .env sets it with $name", + ({ contents }) => + withDotenvDir({ ".env": contents }, (cwd) => + Effect.gen(function* () { + const exit = yield* consent({ shellYes: "1", cwd }).exit; + const error = failureOf(exit); + expect(error).toBeInstanceOf(HostedMutationConsentRequiredError); + expect(error).toMatchObject({ + suggestion: expect.stringContaining( + "SUPABASE_YES in .env does not confirm hosted changes", + ), + }); + }), + ), + ); + + it.effect("grants when the working directory's dotenv files do not mention it", () => + withDotenvDir({ ".env": "OTHER=1\n" }, (cwd) => + Effect.gen(function* () { + const exit = yield* consent({ shellYes: "1", cwd }).exit; + expect(successOf(exit).via).toBe("yes"); + }), + ), + ); + + it.effect("does not grant when a dotenv file that does not parse mentions it", () => + withDotenvDir({ ".env": "SUPABASE_YES=1\n!!!\n" }, (cwd) => + Effect.gen(function* () { + const exit = yield* consent({ shellYes: "1", cwd }).exit; + expect(failureOf(exit)).toBeInstanceOf(HostedMutationConsentRequiredError); + }), + ), + ); + + it.effect("does not grant when the filesystem cannot be checked", () => + Effect.gen(function* () { + const exit = yield* consent({ shellYes: "1", withoutFilesystem: true }).exit; + expect(failureOf(exit)).toBeInstanceOf(HostedMutationConsentRequiredError); + }), + ); + + it.effect("still grants --yes when a working-directory .env sets the variable", () => + withDotenvDir({ ".env": "SUPABASE_YES=1\n" }, (cwd) => + Effect.gen(function* () { + const run = consent({ yes: true, cwd }); + expect(successOf(yield* run.exit).via).toBe("yes"); + expect(run.out.stderrText).toContain("Confirmed by --yes: Apply 3 migrations"); + }), + ), + ); + }); + + describe("when the run cannot prompt", () => { + it.effect.each([ + { + name: "json output", + scenario: { format: "json", stdinIsTty: true }, + reason: "its output format is json", + }, + { + name: "stream-json output", + scenario: { format: "stream-json", stdinIsTty: true }, + reason: "its output format is stream-json", + }, + { + name: "-o yaml in a terminal", + scenario: { ...interactiveTerminal, outputFlag: "yaml" }, + reason: "it writes machine-readable -o yaml output", + }, + { + name: "-o json in a terminal", + scenario: { ...interactiveTerminal, outputFlag: "json" }, + reason: "it writes machine-readable -o json output", + }, + { + name: "piped stdin carrying an affirmative answer", + scenario: { + format: "text", + interactive: true, + stdinIsTty: false, + confirmResponses: [true], + }, + reason: "stdin is not a terminal", + }, + { + name: "a non-interactive stdout", + scenario: { format: "text", interactive: false, stdinIsTty: true }, + reason: "stdout is not a terminal", + }, + ] as const satisfies ReadonlyArray<{ name: string; scenario: Scenario; reason: string }>)( + "fails with ConsentRequiredError for $name and never asks", + ({ scenario, reason }) => + Effect.gen(function* () { + const run = consent({ ...scenario, announced: "project abcdefghijklmnopqrst" }); + const error = failureOf(yield* run.exit); + expect(error).toBeInstanceOf(HostedMutationConsentRequiredError); + expect(error).toMatchObject({ + message: `Confirmation required: Apply 3 migrations (target: project abcdefghijklmnopqrst). This run can't ask because ${reason}. Nothing was changed.`, + suggestion: "Re-run with --yes to confirm.", + }); + expect(run.out.promptConfirmCalls).toEqual([]); + expect(run.out.promptTextCalls).toEqual([]); + }), + ); + + it.effect("leaves the target out of the message when nothing was announced", () => + Effect.gen(function* () { + const error = failureOf(yield* consent({ format: "json" }).exit); + expect(error).toMatchObject({ + message: + "Confirmation required: Apply 3 migrations. This run can't ask because its output format is json. Nothing was changed.", + }); + }), + ); + + it.effect("fails with ConsentRequiredError when the prompt turns out to be unavailable", () => + Effect.gen(function* () { + const error = failureOf(yield* consent({ ...interactiveTerminal, promptFails: true }).exit); + expect(error).toBeInstanceOf(HostedMutationConsentRequiredError); + expect(error).toMatchObject({ + message: expect.stringContaining("because no prompt is available"), + }); + }), + ); + + it.effect("suggests --read-only when the command has a read-only mode", () => + Effect.gen(function* () { + const error = failureOf( + yield* consent({ format: "json" }, { ...REQUEST, readOnlyAlternative: true }).exit, + ); + expect(error).toMatchObject({ + suggestion: + "Re-run with --yes to confirm. For a read-only statement, pass --read-only instead.", + }); + }), + ); + + it.effect("says a project .env SUPABASE_YES does not confirm hosted changes", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = yield* fs.makeTempDirectoryScoped({ prefix: "supabase-consent-" }); + yield* fs.makeDirectory(path.join(dir, "supabase"), { recursive: true }); + yield* fs.writeFileString(path.join(dir, "supabase", ".env"), "SUPABASE_YES=1\n"); + const error = failureOf(yield* consent({ format: "json", cwd: dir, workdir: dir }).exit); + expect(error).toMatchObject({ + suggestion: + "Re-run with --yes to confirm. SUPABASE_YES in supabase/.env does not confirm hosted changes; pass --yes.", + }); + }).pipe(Effect.scoped, Effect.provide(BunServices.layer)), + ); + + it.effect("says a working-directory .env SUPABASE_YES does not confirm hosted changes", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = yield* fs.makeTempDirectoryScoped({ prefix: "supabase-consent-" }); + yield* fs.writeFileString(path.join(dir, ".env"), "SUPABASE_YES=1\n"); + const error = failureOf(yield* consent({ format: "json", shellYes: "1", cwd: dir }).exit); + expect(error).toMatchObject({ + suggestion: + "Re-run with --yes to confirm. SUPABASE_YES in .env does not confirm hosted changes; pass --yes.", + }); + }).pipe(Effect.scoped, Effect.provide(BunServices.layer)), + ); + }); + + describe("agents", () => { + it.effect.each([ + { name: "--agent yes", scenario: { agent: "yes" } }, + { name: "a detected agent", scenario: { agent: "auto", aiTool: "claude_code" } }, + ] as const satisfies ReadonlyArray<{ name: string; scenario: Scenario }>)( + "refuses to prompt for $name in an interactive terminal", + ({ scenario }) => + Effect.gen(function* () { + const run = consent({ ...interactiveTerminal, ...scenario, confirmResponses: [true] }); + const error = failureOf(yield* run.exit); + expect(error).toBeInstanceOf(HostedMutationConsentRequiredError); + expect(error).toMatchObject({ + message: expect.stringContaining("because it is running in agent mode"), + suggestion: "This is an agent run: ask the user before re-running with --yes.", + }); + expect(run.out.promptConfirmCalls).toEqual([]); + }), + ); + + it.effect("prompts when --agent no overrides a detected agent", () => + Effect.gen(function* () { + const run = consent({ + ...interactiveTerminal, + agent: "no", + aiTool: "claude_code", + confirmResponses: [true], + }); + expect(successOf(yield* run.exit).via).toBe("prompt"); + }), + ); + + it.effect("grants --yes to an agent", () => + Effect.gen(function* () { + const run = consent({ ...interactiveTerminal, yes: true, agent: "yes" }); + expect(successOf(yield* run.exit).via).toBe("yes"); + }), + ); + }); + + describe("in an interactive terminal", () => { + it.effect("grants an accepted prompt and prints the details first", () => + Effect.gen(function* () { + const run = consent({ + ...interactiveTerminal, + confirmResponses: [true], + announced: "project abcdefghijklmnopqrst", + }); + const token = successOf(yield* run.exit); + expect(token).toMatchObject({ action: "Apply 3 migrations", via: "prompt" }); + expect(run.out.stderrText).toBe( + "Target: project abcdefghijklmnopqrst\n 20240101_a.sql\n 20240102_b.sql\n", + ); + expect(run.out.promptConfirmCalls.map((call) => call.message)).toEqual([ + "Apply 3 migrations on project abcdefghijklmnopqrst?", + ]); + }), + ); + + it.effect("fails with DeclinedError when the answer is no", () => + Effect.gen(function* () { + const run = consent({ ...interactiveTerminal, confirmResponses: [false] }); + const error = failureOf(yield* run.exit); + expect(error).toBeInstanceOf(HostedMutationDeclinedError); + expect(error).toMatchObject({ message: "context canceled" }); + }), + ); + + describe("typed confirmation", () => { + const typed: HostedMutationRequest = { + action: "delete compute api", + details: ["This permanently deletes the compute."], + confirm: { kind: "typed", expected: "api" }, + }; + + it.effect("grants when the typed answer matches", () => + Effect.gen(function* () { + const run = consent({ ...interactiveTerminal, textResponses: [" api "] }, typed); + const token = successOf(yield* run.exit); + expect(token.via).toBe("prompt"); + expect(run.out.promptTextCalls.map((call) => call.message)).toEqual([ + "Type api to confirm", + ]); + expect(run.out.promptConfirmCalls).toEqual([]); + expect(run.out.stderrText).toBe( + "This permanently deletes the compute.\ndelete compute api\n", + ); + }), + ); + + it.effect("fails with DeclinedError when the typed answer differs", () => + Effect.gen(function* () { + const run = consent({ ...interactiveTerminal, textResponses: ["apis"] }, typed); + expect(failureOf(yield* run.exit)).toBeInstanceOf(HostedMutationDeclinedError); + }), + ); + }); + }); + + it("classifies the errors for telemetry", () => { + expect( + classifyCliErrorActionability( + new HostedMutationConsentRequiredError({ message: "m", suggestion: "s" }), + ), + ).toMatchObject({ + error_kind: "user_actionable", + error_category: "consent_required", + has_suggestion: true, + suggestion_type: "provide_flags", + }); + expect( + classifyCliErrorActionability(new HostedMutationDeclinedError({ message: "m" })), + ).toMatchObject({ error_kind: "user_cancelled", error_category: "cancelled" }); + }); +}); + +describe("hostedMutationPreapproval", () => { + const preapproval = (scenario: Scenario) => harness(scenario).prepare(hostedMutationPreapproval); + + it.effect("is Some under --yes", () => + Effect.gen(function* () { + expect(Option.isSome(yield* preapproval({ yes: true }))).toBe(true); + }), + ); + + it.effect("is Some under a shell SUPABASE_YES", () => + Effect.gen(function* () { + expect(Option.isSome(yield* preapproval({ shellYes: "1" }))).toBe(true); + }), + ); + + it.effect("is None in a terminal that could prompt", () => + Effect.gen(function* () { + const { out, prepare } = harness({ ...interactiveTerminal, confirmResponses: [true] }); + expect(Option.isNone(yield* prepare(hostedMutationPreapproval))).toBe(true); + expect(out.promptConfirmCalls).toEqual([]); + }), + ); + + it.effect("is None under --yes=false", () => + Effect.gen(function* () { + expect(Option.isNone(yield* preapproval({ args: ["--yes=false"], shellYes: "1" }))).toBe( + true, + ); + }), + ); + + it.effect("is None for a shell value that a working-directory .env also sets", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "supabase-consent-" }); + yield* fs.writeFileString(path.join(cwd, ".env.local"), "SUPABASE_YES=1\n"); + expect(Option.isNone(yield* preapproval({ shellYes: "1", cwd }))).toBe(true); + }).pipe(Effect.scoped, Effect.provide(BunServices.layer)), + ); +}); + +describe("requireTargetMutationConsent", () => { + const dbConfig = (isLocal: boolean): ResolvedDbConfig => ({ + conn: { + host: "db.example.com", + port: 5432, + user: "postgres", + password: "", + database: "postgres", + }, + isLocal, + }); + const linked: ResolvedTarget = { + kind: "linked", + ref: "abcdefghijklmnopqrst", + source: "file", + matchesLinkedRef: true, + }; + + const forTarget = (target: ResolvedTarget | ResolvedDbConfig, scenario: Scenario = {}) => { + const { out, prepare } = harness(scenario); + return { out, exit: prepare(requireTargetMutationConsent(target, REQUEST)).pipe(Effect.exit) }; + }; + + it.effect.each([ + { name: "a local target", target: { kind: "local" } satisfies ResolvedTarget }, + { name: "a connection to the local stack", target: dbConfig(true) }, + ])("is None for $name without asking or echoing", ({ target }) => + Effect.gen(function* () { + const run = forTarget(target, { ...interactiveTerminal, yes: true }); + const result = successOf(yield* run.exit); + expect(Option.isNone(result)).toBe(true); + expect(run.out.promptConfirmCalls).toEqual([]); + expect(run.out.stderrText).toBe(""); + }), + ); + + it.effect.each([ + { name: "a linked target", target: linked }, + { + name: "an unclassified --db-url target", + target: { + kind: "db-url", + url: Redacted.make("postgresql://postgres:secret@db.example.com:5432/postgres"), + } satisfies ResolvedTarget, + }, + { name: "a connection to a hosted database", target: dbConfig(false) }, + ])("asks for $name", ({ target }) => + Effect.gen(function* () { + const run = forTarget(target, { ...interactiveTerminal, confirmResponses: [true] }); + const result = successOf(yield* run.exit); + expect(Option.map(result, (token) => token.via)).toEqual(Option.some("prompt")); + }), + ); + + it.effect("fails for a hosted target in a run that cannot prompt", () => + Effect.gen(function* () { + const error = failureOf(yield* forTarget(linked, { format: "json" }).exit); + expect(error).toBeInstanceOf(HostedMutationConsentRequiredError); + }), + ); +}); diff --git a/apps/cli/src/command-internal/hosted-consent/hosted-consent.yes.ts b/apps/cli/src/command-internal/hosted-consent/hosted-consent.yes.ts new file mode 100644 index 0000000000..3bff9f513f --- /dev/null +++ b/apps/cli/src/command-internal/hosted-consent/hosted-consent.yes.ts @@ -0,0 +1,98 @@ +import { Effect, FileSystem, Option, Path } from "effect"; + +import { CommandSettings } from "../../config/command-settings.service.ts"; +import { candidateDotenvFilenames } from "../../shared/config/cli-config-env.ts"; +import { parseDotEnv } from "../../shared/config/dotenv.ts"; +import { envValue } from "../../shared/config/env-option.ts"; +import { RuntimeInfo } from "../../shared/runtime/runtime-info.service.ts"; +import { resolveYesSource } from "../global-flags.ts"; +import { parseEnvBool } from "../supabase-env.ts"; + +export type HostedYesSource = "--yes" | "SUPABASE_YES"; + +const YES_VARIABLE = "SUPABASE_YES"; + +interface DotenvDefinition { + readonly file: string; + /** `None` when the file does not parse but mentions the variable, which counts as any value. */ + readonly value: Option.Option; +} + +interface Platform { + readonly fs: FileSystem.FileSystem; + readonly path: Path.Path; + readonly cwd: string; +} + +// Without a filesystem the origin of `SUPABASE_YES` cannot be checked, so callers treat it as +// unverified. +const platform = Effect.gen(function* () { + const fs = yield* Effect.serviceOption(FileSystem.FileSystem); + const path = yield* Effect.serviceOption(Path.Path); + const runtime = yield* Effect.serviceOption(RuntimeInfo); + return Option.all({ fs, path, runtime }).pipe( + Option.map(({ fs, path, runtime }): Platform => ({ fs, path, cwd: runtime.cwd })), + ); +}); + +const envNameFrom = (variable: string) => + Effect.map(envValue(variable), (value) => + value === undefined || value === "" ? "development" : value, + ); + +const definitionsIn = Effect.fnUntraced(function* (host: Platform, dir: string, env: string) { + const found: Array = []; + for (const name of candidateDotenvFilenames(env)) { + const file = host.path.join(dir, name); + const content = yield* host.fs.readFileString(file).pipe(Effect.option); + if (Option.isNone(content)) continue; + const parsed = yield* Effect.try(() => parseDotEnv(content.value)).pipe(Effect.option); + if (Option.isNone(parsed)) { + if (content.value.includes(YES_VARIABLE)) found.push({ file, value: Option.none() }); + } else if (Object.hasOwn(parsed.value, YES_VARIABLE)) { + found.push({ file, value: Option.some(parsed.value[YES_VARIABLE]!) }); + } + } + return found; +}); + +/** The files Bun loads into `process.env` from the working directory before any CLI code runs. */ +const autoloadedDefinitions = (host: Platform) => + Effect.flatMap(envNameFrom("NODE_ENV"), (env) => definitionsIn(host, host.cwd, env)); + +/** + * Confirms a hosted change only for `--yes` or a `SUPABASE_YES` the shell exported. Bun copies + * working-directory dotenv files into the environment and its parsing can differ from ours, so a + * working-directory dotenv file that defines `SUPABASE_YES` at all, whatever its value, makes the + * environment's value the file's and not consent. + */ +export const resolveHostedYes = Effect.gen(function* () { + const source = yield* resolveYesSource; + if (Option.isNone(source)) return Option.none(); + if (source.value.via === "flag") return Option.some("--yes"); + const host = yield* platform; + if (Option.isNone(host)) return Option.none(); + const fromFile = (yield* autoloadedDefinitions(host.value)).length > 0; + return fromFile ? Option.none() : Option.some("SUPABASE_YES"); +}); + +/** Dotenv files, relative to the cwd, whose `SUPABASE_YES` does not confirm a hosted change. */ +export const ignoredYesFiles = Effect.gen(function* () { + const host = yield* platform; + if (Option.isNone(host)) return []; + const { path, cwd } = host.value; + const workdir = Option.match(yield* Effect.serviceOption(CommandSettings), { + onNone: () => cwd, + onSome: (settings) => settings.workdir, + }); + const projectEnv = yield* envNameFrom("SUPABASE_ENV"); + + const projectDefinitions = [ + ...(yield* definitionsIn(host.value, path.join(workdir, "supabase"), projectEnv)), + ...(yield* definitionsIn(host.value, workdir, projectEnv)), + ].filter(({ value }) => Option.isNone(value) || parseEnvBool(value.value)); + const files = [...(yield* autoloadedDefinitions(host.value)), ...projectDefinitions].map( + ({ file }) => path.relative(cwd, file), + ); + return [...new Set(files)]; +}); diff --git a/apps/cli/src/command-internal/hosted-versions.ts b/apps/cli/src/command-internal/hosted-versions.ts index f53a813f91..c14b0ee80c 100644 --- a/apps/cli/src/command-internal/hosted-versions.ts +++ b/apps/cli/src/command-internal/hosted-versions.ts @@ -3,7 +3,7 @@ import { Effect, Option } from "effect"; import * as HttpClientError from "effect/http/HttpClientError"; import { CommandSettings } from "../config/command-settings.service.ts"; -import type { ProjectLink } from "./project-link.ts"; +import type { ProjectLink } from "../shared/config/project-link.ts"; import type { HostedProbe } from "./stack-lock.ts"; import { extractServiceKeys } from "./tenant-keys.ts"; import { probeFailure, probeTenantVersions, type HostedObservation } from "./tenant-versions.ts"; diff --git a/apps/cli/src/command-internal/hostname.ts b/apps/cli/src/command-internal/hostname.ts index 9d1a7ac23a..857353d9b1 100644 --- a/apps/cli/src/command-internal/hostname.ts +++ b/apps/cli/src/command-internal/hostname.ts @@ -1,30 +1,23 @@ import { Config, Crypto, Effect, FileSystem, Option, Path, Schema } from "effect"; +import type { ResolvedCliConfig } from "../config/cli-config-values.service.ts"; +import type { CliConfigLoadError } from "../shared/config/cli-config-load.errors.ts"; import { RuntimeInfo } from "../shared/runtime/runtime-info.service.ts"; +import { ambientEnvironment } from "../shared/config/cli-config-provider.layer.ts"; const LOCAL_HOST = "127.0.0.1"; const LOOPBACK_NO_PROXY = `localhost,${LOCAL_HOST},[::1]`; const DEFAULT_CONTEXT_NAME = "default"; -type Environment = Readonly>; +// The docker client only sees the shell env, so daemon discovery never reads a project `.env*`. +const shellOption = (name: string): Effect.Effect, Config.ConfigError> => + Config.option(Config.String(name)); -const envOption = ( - name: string, - projectEnvValues?: Environment, -): Effect.Effect, Config.ConfigError> => { - const projectValue = Option.fromNullishOr(projectEnvValues?.[name]); - return Option.isSome(projectValue) - ? Effect.succeed(projectValue) - : Config.option(Config.String(name)); -}; - -const dockerConfigDir = ( - projectEnvValues?: Environment, -): Effect.Effect => +const dockerConfigDir = (): Effect.Effect => Effect.gen(function* () { const runtime = yield* RuntimeInfo; const path = yield* Path.Path; - const configured = yield* envOption("DOCKER_CONFIG", projectEnvValues); + const configured = yield* shellOption("DOCKER_CONFIG"); return Option.getOrElse(configured.pipe(Option.filter((value) => value.length > 0)), () => path.join(runtime.homeDir, ".docker"), ); @@ -51,15 +44,17 @@ const decodeDockerMeta = ( ): Effect.Effect>> => Schema.decodeEffect(Schema.fromJsonString(DockerMetaSchema))(content).pipe(Effect.option); -const currentDockerContextName = ( - projectEnvValues?: Environment, -): Effect.Effect => +const currentDockerContextName = (): Effect.Effect< + string, + Config.ConfigError, + RuntimeInfo | FileSystem.FileSystem | Path.Path +> => Effect.gen(function* () { - const fromEnv = yield* envOption("DOCKER_CONTEXT", projectEnvValues); + const fromEnv = yield* shellOption("DOCKER_CONTEXT"); if (Option.isSome(fromEnv) && fromEnv.value.length > 0) return fromEnv.value; const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const configDir = yield* dockerConfigDir(projectEnvValues); + const configDir = yield* dockerConfigDir(); const configPath = path.join(configDir, "config.json"); const config = yield* fs.readFileString(configPath).pipe(Effect.option); @@ -73,13 +68,13 @@ const currentDockerContextName = ( return DEFAULT_CONTEXT_NAME; }); -const dockerContextEndpointHost = (contextName: string, projectEnvValues?: Environment) => { +const dockerContextEndpointHost = (contextName: string) => { if (contextName === DEFAULT_CONTEXT_NAME) return Effect.succeed(Option.none()); return Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const crypto = yield* Crypto.Crypto; - const configDir = yield* dockerConfigDir(projectEnvValues); + const configDir = yield* dockerConfigDir(); const digest = yield* crypto .digest("SHA-256", new TextEncoder().encode(contextName)) .pipe(Effect.option); @@ -122,36 +117,35 @@ export function platformDefaultDockerHost(platform: NodeJS.Platform): string { } /** Resolves the daemon endpoint selected by the Docker CLI. */ -export const resolveDockerDaemonEndpoint = ( - projectEnvValues?: Environment, -): Effect.Effect< +export const resolveDockerDaemonEndpoint = Effect.fnUntraced(function* (): Effect.fn.Return< Option.Option, Config.ConfigError, RuntimeInfo | FileSystem.FileSystem | Path.Path | Crypto.Crypto -> => - Effect.gen(function* () { - const runtime = yield* RuntimeInfo; - const dockerHost = yield* envOption("DOCKER_HOST", projectEnvValues); - if (Option.isSome(dockerHost) && dockerHost.value.length > 0) return dockerHost; - const contextName = yield* currentDockerContextName(projectEnvValues); - if (contextName === DEFAULT_CONTEXT_NAME) { - return Option.some(platformDefaultDockerHost(runtime.platform)); - } - return yield* dockerContextEndpointHost(contextName, projectEnvValues); - }); +> { + const runtime = yield* RuntimeInfo; + const dockerHost = yield* shellOption("DOCKER_HOST"); + if (Option.isSome(dockerHost) && dockerHost.value.length > 0) return dockerHost; + const contextName = yield* currentDockerContextName(); + if (contextName === DEFAULT_CONTEXT_NAME) { + return Option.some(platformDefaultDockerHost(runtime.platform)); + } + return yield* dockerContextEndpointHost(contextName); +}); -/** Resolves the hostname used for local Supabase service connections. */ +/** Resolves the hostname for local service connections: `SUPABASE_SERVICES_HOSTNAME` (shell, then project `.env*`), else the shell-selected Docker daemon's tcp host. */ export const getHostname = ( - projectEnvValues?: Environment, + resolvedConfig: Pick, ): Effect.Effect< string, - Config.ConfigError, + Config.ConfigError | CliConfigLoadError, RuntimeInfo | FileSystem.FileSystem | Path.Path | Crypto.Crypto > => Effect.gen(function* () { - const override = yield* envOption("SUPABASE_SERVICES_HOSTNAME", projectEnvValues); - if (Option.isSome(override) && override.value.length > 0) return override.value; - const endpoint = yield* resolveDockerDaemonEndpoint(projectEnvValues); + const { SUPABASE_SERVICES_HOSTNAME: override } = yield* resolvedConfig.envValues([ + "SUPABASE_SERVICES_HOSTNAME", + ]); + if (override !== undefined) return override; + const endpoint = yield* resolveDockerDaemonEndpoint(); if (Option.isSome(endpoint)) { const host = yield* hostFromTcpEndpoint(endpoint.value); if (Option.isSome(host)) return host.value; @@ -160,7 +154,7 @@ export const getHostname = ( }); /** Keeps Bun from proxying the CLI's loopback HTTP requests. */ -export function configureLoopbackProxyBypass(env: NodeJS.ProcessEnv = process.env): void { +export function configureLoopbackProxyBypass(env: NodeJS.ProcessEnv = ambientEnvironment()): void { const key = (env["no_proxy"]?.length ?? 0) > 0 ? "no_proxy" : "NO_PROXY"; const current = env[key]; env[key] = current ? `${current},${LOOPBACK_NO_PROXY}` : LOOPBACK_NO_PROXY; diff --git a/apps/cli/src/command-internal/hostname.unit.test.ts b/apps/cli/src/command-internal/hostname.unit.test.ts index 712fbdb58b..72d6f8d273 100644 --- a/apps/cli/src/command-internal/hostname.unit.test.ts +++ b/apps/cli/src/command-internal/hostname.unit.test.ts @@ -13,6 +13,7 @@ import { Path, Schema, } from "effect"; +import type { CliConfigLoadError } from "../shared/config/cli-config-load.errors.ts"; import { RuntimeInfo } from "../shared/runtime/runtime-info.service.ts"; import { configureLoopbackProxyBypass, @@ -37,6 +38,20 @@ const runtimeLayer = Layer.succeed( type HostnameServices = RuntimeInfo | FileSystem.FileSystem | Path.Path | Crypto.Crypto; +/** A resolved config whose env-tier lookup yields `env`; the real tier order is covered by the config service tests. */ +const resolvedConfigWith = (env: Readonly> = {}) => ({ + envValues: (names: Iterable) => + Effect.succeed( + Object.fromEntries( + [...names].flatMap((name) => { + const value = env[name]; + return value === undefined || value === "" ? [] : [[name, value]]; + }), + ), + ), +}); +const noConfig = resolvedConfigWith(); + function configLayer(env: Readonly>) { return Layer.mergeAll( BunServices.layer, @@ -75,7 +90,7 @@ const writeDockerConfigDir = Effect.fnUntraced(function* (options: { function withDockerConfig( options: Parameters[0], env: Readonly>, - run: () => Effect.Effect, + run: () => Effect.Effect, ) { return writeDockerConfigDir(options).pipe( Effect.provide(BunServices.layer), @@ -86,29 +101,35 @@ function withDockerConfig( } describe("getHostname", () => { - it.effect("prefers a project override and preserves an explicit empty value", () => + it.effect("uses the resolved SUPABASE_SERVICES_HOSTNAME and ignores an empty one", () => Effect.gen(function* () { - expect(yield* getHostname({ SUPABASE_SERVICES_HOSTNAME: "db.internal" })).toBe("db.internal"); - expect(yield* getHostname({ SUPABASE_SERVICES_HOSTNAME: "" })).toBe("127.0.0.1"); + expect( + yield* getHostname(resolvedConfigWith({ SUPABASE_SERVICES_HOSTNAME: "db.internal" })), + ).toBe("db.internal"); + expect(yield* getHostname(resolvedConfigWith({ SUPABASE_SERVICES_HOSTNAME: "" }))).toBe( + "127.0.0.1", + ); }).pipe(Effect.provide(configLayer({}))), ); - it.effect("reads ambient DOCKER_HOST and extracts IPv4 and IPv6 hosts", () => + it.effect("prefers the resolved hostname over a tcp DOCKER_HOST", () => Effect.gen(function* () { - expect(yield* getHostname()).toBe("docker-host"); - expect(yield* getHostname({})).toBe("docker-host"); - expect(yield* getHostname({ DOCKER_HOST: "tcp://[::1]:2375" })).toBe("::1"); - }).pipe( - Effect.provide( - Layer.mergeAll( - BunServices.layer, - runtimeLayer, - ConfigProvider.layer( - ConfigProvider.fromEnvRecord({ DOCKER_HOST: "tcp://docker-host:2375" }), - ), - ), - ), - ), + expect( + yield* getHostname(resolvedConfigWith({ SUPABASE_SERVICES_HOSTNAME: "override" })), + ).toBe("override"); + }).pipe(Effect.provide(configLayer({ DOCKER_HOST: "tcp://docker-host:2375" }))), + ); + + it.effect("extracts the host from an ambient tcp DOCKER_HOST", () => + Effect.gen(function* () { + expect(yield* getHostname(noConfig)).toBe("docker-host"); + }).pipe(Effect.provide(configLayer({ DOCKER_HOST: "tcp://docker-host:2375" }))), + ); + + it.effect("extracts an IPv6 host from an ambient tcp DOCKER_HOST", () => + Effect.gen(function* () { + expect(yield* getHostname(noConfig)).toBe("::1"); + }).pipe(Effect.provide(configLayer({ DOCKER_HOST: "tcp://[::1]:2375" }))), ); it.effect("uses currentContext from Docker config", () => @@ -117,7 +138,7 @@ describe("getHostname", () => { {}, () => Effect.gen(function* () { - expect(yield* getHostname()).toBe("remote-host"); + expect(yield* getHostname(noConfig)).toBe("remote-host"); }), ), ); @@ -131,7 +152,7 @@ describe("getHostname", () => { { DOCKER_CONTEXT: "envctx" }, () => Effect.gen(function* () { - expect(yield* getHostname()).toBe("envctx-host"); + expect(yield* getHostname(noConfig)).toBe("envctx-host"); }), ), ); @@ -142,7 +163,7 @@ describe("getHostname", () => { {}, () => Effect.gen(function* () { - expect(yield* getHostname()).toBe("::1"); + expect(yield* getHostname(noConfig)).toBe("::1"); }), ), ); @@ -153,7 +174,7 @@ describe("getHostname", () => { {}, () => Effect.gen(function* () { - expect(yield* getHostname()).toBe("127.0.0.1"); + expect(yield* getHostname(noConfig)).toBe("127.0.0.1"); }), ), ); @@ -162,7 +183,7 @@ describe("getHostname", () => { withDockerConfig({ currentContext: "ghost" }, {}, () => Effect.gen(function* () { expect(yield* resolveDockerDaemonEndpoint()).toEqual(Option.none()); - expect(yield* getHostname()).toBe("127.0.0.1"); + expect(yield* getHostname(noConfig)).toBe("127.0.0.1"); }), ), ); @@ -170,7 +191,7 @@ describe("getHostname", () => { it.effect("falls back to the default context when Docker config is missing", () => withDockerConfig({}, {}, () => Effect.gen(function* () { - expect(yield* getHostname()).toBe("127.0.0.1"); + expect(yield* getHostname(noConfig)).toBe("127.0.0.1"); }), ), ); @@ -184,7 +205,7 @@ describe("getHostname", () => { expect(yield* resolveDockerDaemonEndpoint()).toEqual( Option.some("unix:///var/run/docker.sock"), ); - expect(yield* getHostname()).toBe("127.0.0.1"); + expect(yield* getHostname(noConfig)).toBe("127.0.0.1"); }), ), ); @@ -195,7 +216,7 @@ describe("getHostname", () => { { DOCKER_HOST: "tcp://direct-host:2375" }, () => Effect.gen(function* () { - expect(yield* getHostname()).toBe("direct-host"); + expect(yield* getHostname(noConfig)).toBe("direct-host"); }), ), ); @@ -215,7 +236,7 @@ describe("getHostname", () => { it.effect("returns loopback for an absent or non-tcp endpoint", () => Effect.gen(function* () { - expect(yield* getHostname()).toBe("127.0.0.1"); + expect(yield* getHostname(noConfig)).toBe("127.0.0.1"); }).pipe( Effect.provide( Layer.mergeAll( @@ -233,13 +254,16 @@ describe("getHostname", () => { describe("resolveDockerDaemonEndpoint", () => { it.effect("returns DOCKER_HOST verbatim, including non-tcp schemes", () => Effect.gen(function* () { - expect( - yield* resolveDockerDaemonEndpoint({ DOCKER_HOST: "unix:///custom/engine.sock" }), - ).toEqual(Option.some("unix:///custom/engine.sock")); - expect(yield* resolveDockerDaemonEndpoint({ DOCKER_HOST: "tcp://docker-host:2375" })).toEqual( - Option.some("tcp://docker-host:2375"), + expect(yield* resolveDockerDaemonEndpoint()).toEqual( + Option.some("unix:///custom/engine.sock"), ); - }).pipe(Effect.provide(configLayer({}))), + }).pipe(Effect.provide(configLayer({ DOCKER_HOST: "unix:///custom/engine.sock" }))), + ); + + it.effect("returns a tcp DOCKER_HOST verbatim", () => + Effect.gen(function* () { + expect(yield* resolveDockerDaemonEndpoint()).toEqual(Option.some("tcp://docker-host:2375")); + }).pipe(Effect.provide(configLayer({ DOCKER_HOST: "tcp://docker-host:2375" }))), ); it.effect("maps the default context to the platform default daemon endpoint", () => diff --git a/apps/cli/src/command-internal/go-json.ts b/apps/cli/src/command-internal/html-safe-json.ts similarity index 65% rename from apps/cli/src/command-internal/go-json.ts rename to apps/cli/src/command-internal/html-safe-json.ts index 1d9d8a3b89..39652e6361 100644 --- a/apps/cli/src/command-internal/go-json.ts +++ b/apps/cli/src/command-internal/html-safe-json.ts @@ -1,13 +1,12 @@ /** - * Byte-faithful reproduction of `encoding/json`'s encoder, for commands that must match Go's - * stdout exactly. + * Byte-stable JSON encoder for commands whose stdout format is fixed. * * Doesn't sort object keys — the caller builds objects whose key insertion order is already the * desired order; a `Map` is preserved as true insertion order (a plain object reorders * integer-like string keys into ascending numeric order, which would undo a lexicographic sort). * * Differs from `JSON.stringify(x, null, 2)` in escaping `<`, `>`, `&`, and control characters - * (`\u0008`/`\u000c` instead of `\b`/`\f`) the way Go's default encoder does. + * (`\u0008`/`\u000c` instead of `\b`/`\f`). */ const HEX = "0123456789abcdef"; @@ -17,11 +16,11 @@ function unicodeEscape(codeUnit: number): string { } /** - * Quotes and escapes a string exactly as `encoding/json` does with default HTML escaping. - * Iterates by UTF-16 code unit; the only non-ASCII runes Go escapes (U+2028, U+2029) are single + * Quotes and escapes a string with HTML escaping. + * Iterates by UTF-16 code unit; the only non-ASCII characters escaped (U+2028, U+2029) are single * BMP code units, so code units suffice. */ -export function escapeGoJsonString(value: string): string { +export function escapeHtmlSafeJsonString(value: string): string { let out = '"'; for (let i = 0; i < value.length; i++) { const code = value.charCodeAt(i); @@ -65,10 +64,10 @@ function walk(value: unknown, depth: number, pretty: boolean): string { if (value === null || value === undefined) return "null"; switch (typeof value) { case "string": - return escapeGoJsonString(value); + return escapeHtmlSafeJsonString(value); case "number": - // JSON.stringify collapses `-0` to `"0"`, but `encoding/json` marshals a float64 negative - // zero as `-0` — reachable via `gen bearer-jwt`'s signed payload, where the signed bytes + // JSON.stringify collapses `-0` to `"0"`, but a float64 negative + // zero marshals as `-0` — reachable via `gen bearer-jwt`'s signed payload, where the signed bytes // must match. if (!Number.isFinite(value)) return "null"; return Object.is(value, -0) ? "-0" : JSON.stringify(value); @@ -86,7 +85,7 @@ function walk(value: unknown, depth: number, pretty: boolean): string { return `[${open}${items.join(separator)}${close}${closeIndent}]`; } // A plain object reorders integer-like string keys ("2", "10") into ascending numeric order on - // enumeration, unlike a real Go map's lexicographic order; callers needing that order (e.g. + // enumeration, unlike lexicographic order; callers needing that order (e.g. // `sortKeysDeep`) pass a `Map`, whose iteration order is true insertion order. const entries = value instanceof Map @@ -95,34 +94,34 @@ function walk(value: unknown, depth: number, pretty: boolean): string { if (entries.length === 0) return "{}"; const colon = pretty ? ": " : ":"; const lines = entries.map( - ([key, val]) => `${indent}${escapeGoJsonString(key)}${colon}${walk(val, depth + 1, pretty)}`, + ([key, val]) => + `${indent}${escapeHtmlSafeJsonString(key)}${colon}${walk(val, depth + 1, pretty)}`, ); return `{${open}${lines.join(separator)}${close}${closeIndent}}`; } /** - * Encodes a value the way `json.Encoder` with `SetIndent("", " ")` + - * `Encode` does: 2-space indentation, object keys in insertion (struct) order, - * Go string escaping, and a trailing newline. + * Encodes a value with 2-space indentation, object keys in insertion order, + * HTML-safe string escaping, and a trailing newline. */ -export function encodeGoJsonIndented(value: unknown): string { +export function encodeHtmlSafeJsonIndented(value: unknown): string { return walk(value, 0, true) + "\n"; } /** - * Encodes a value the way `json.Marshal` does: compact separators - * (`{"k":v}`), object keys in insertion (struct) order, Go string escaping - * (HTML characters included), and no trailing newline. + * Encodes a value with compact separators + * (`{"k":v}`), object keys in insertion order, HTML-safe string escaping, + * and no trailing newline. */ -export function encodeGoJsonCompact(value: unknown): string { +export function encodeHtmlSafeJsonCompact(value: unknown): string { return walk(value, 0, false); } /** - * `encoding/json` type names for the JSON-representable kinds `json.Unmarshal` rejects. Used to - * reproduce Go's exact `"json: cannot unmarshal into Go value of type "` message. + * Type names for the JSON-representable kinds a decode rejects, used in "expected X, got " + * decode errors. */ -export function goJsonKindName(value: unknown): string { +export function jsonKindName(value: unknown): string { if (Array.isArray(value)) return "array"; switch (typeof value) { case "number": @@ -130,7 +129,7 @@ export function goJsonKindName(value: unknown): string { case "string": return "string"; case "boolean": - return "bool"; + return "boolean"; default: return "value"; } diff --git a/apps/cli/src/command-internal/html-safe-json.unit.test.ts b/apps/cli/src/command-internal/html-safe-json.unit.test.ts new file mode 100644 index 0000000000..733067c7c8 --- /dev/null +++ b/apps/cli/src/command-internal/html-safe-json.unit.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from "vitest"; + +import { + encodeHtmlSafeJsonCompact, + encodeHtmlSafeJsonIndented, + escapeHtmlSafeJsonString, + jsonKindName, +} from "./html-safe-json.ts"; + +describe("escapeHtmlSafeJsonString", () => { + it("escapes quotes and backslashes", () => { + expect(escapeHtmlSafeJsonString(`a"b\\c`)).toBe('"a\\"b\\\\c"'); + }); + + it("HTML-escapes <, > and &", () => { + expect(escapeHtmlSafeJsonString(" & ")).toBe('"\\u003ca\\u003e \\u0026 \\u003cb\\u003e"'); + }); + + it("uses short escapes for tab/newline/carriage-return", () => { + expect(escapeHtmlSafeJsonString("a\tb\nc\rd")).toBe('"a\\tb\\nc\\rd"'); + }); + + it("uses \\u00xx for other control characters (no \\b / \\f shorthand)", () => { + expect(escapeHtmlSafeJsonString("\b\f")).toBe('"\\u0008\\u000c"'); + }); + + it("escapes U+2028 and U+2029", () => { + expect(escapeHtmlSafeJsonString("

")).toBe('"\\u2028\\u2029"'); + }); +}); + +describe("encodeHtmlSafeJsonIndented", () => { + it("preserves object key insertion order (not alphabetical)", () => { + expect(encodeHtmlSafeJsonIndented({ level: "error", message: "boom" })).toBe( + `{\n "level": "error",\n "message": "boom"\n}\n`, + ); + }); + + it("renders nested arrays of objects with 2-space indent and a trailing newline", () => { + const value = [{ function: "public.f1", issues: [{ level: "error", message: "test 1b" }] }]; + expect(encodeHtmlSafeJsonIndented(value)).toBe( + [ + "[", + " {", + ' "function": "public.f1",', + ' "issues": [', + " {", + ' "level": "error",', + ' "message": "test 1b"', + " }", + " ]", + " }", + "]", + "", + ].join("\n"), + ); + }); + + it("renders empty arrays and objects compactly", () => { + expect(encodeHtmlSafeJsonIndented([])).toBe("[]\n"); + expect(encodeHtmlSafeJsonIndented({})).toBe("{}\n"); + expect(encodeHtmlSafeJsonIndented({ issues: [] })).toBe(`{\n "issues": []\n}\n`); + }); +}); + +describe("encodeHtmlSafeJsonCompact", () => { + it("emits the compact shape with HTML escaping", () => { + expect(encodeHtmlSafeJsonCompact({ metadata_xml: "&stuff", type: "saml" })).toBe( + '{"metadata_xml":"\\u003cxml\\u003e\\u0026stuff\\u003c/xml\\u003e","type":"saml"}', + ); + }); + + it("keeps insertion order, compact separators, and no trailing newline", () => { + expect(encodeHtmlSafeJsonCompact({ b: [1, 2], a: { c: true } })).toBe( + '{"b":[1,2],"a":{"c":true}}', + ); + expect(encodeHtmlSafeJsonCompact([])).toBe("[]"); + expect(encodeHtmlSafeJsonCompact(null)).toBe("null"); + }); + + it("preserves negative zero's sign, unlike plain JSON.stringify", () => { + expect(encodeHtmlSafeJsonCompact({ extra: -0 })).toBe('{"extra":-0}'); + expect(encodeHtmlSafeJsonCompact({ extra: Number("-1e-10000") })).toBe('{"extra":-0}'); + expect(encodeHtmlSafeJsonCompact({ extra: 0 })).toBe('{"extra":0}'); + }); + + it("iterates a Map in true insertion order, unlike a plain object with integer-like keys", () => { + const map = new Map([ + ["10", "a"], + ["2", "b"], + ]); + expect(encodeHtmlSafeJsonCompact(map)).toBe('{"10":"a","2":"b"}'); + }); +}); + +describe("jsonKindName", () => { + it("names every JSON-representable kind, including the generic fallback", () => { + expect(jsonKindName([])).toBe("array"); + expect(jsonKindName(1)).toBe("number"); + expect(jsonKindName("s")).toBe("string"); + expect(jsonKindName(true)).toBe("boolean"); + expect(jsonKindName(undefined)).toBe("value"); + }); +}); diff --git a/apps/cli/src/command-internal/legacy-project-state.unit.test.ts b/apps/cli/src/command-internal/legacy-project-state.unit.test.ts deleted file mode 100644 index de9281a443..0000000000 --- a/apps/cli/src/command-internal/legacy-project-state.unit.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { BunServices } from "@effect/platform-bun"; -import { describe, expect, it } from "@effect/vitest"; -import { Effect, FileSystem, Layer, Option, Path } from "effect"; - -import { classifyCliErrorActionability } from "../shared/telemetry/error-actionability.ts"; -import { legacyProjectStateLayer } from "./legacy-project-state.layer.ts"; -import { legacyProjectStatePaths } from "./legacy-project-state.paths.ts"; -import { LegacyProjectState, ProjectRefReadError } from "./legacy-project-state.service.ts"; - -const services = legacyProjectStateLayer.pipe(Layer.provideMerge(BunServices.layer)); - -const readRef = (workdir: string) => - Effect.gen(function* () { - const state = yield* LegacyProjectState; - return yield* state.readProjectRef(workdir); - }); - -const REF = "abcdefghijklmnopqrst"; - -describe("legacyProjectStatePaths", () => { - it.effect("maps a workdir to the supabase/.temp/* layout", () => - Effect.gen(function* () { - const path = yield* Path.Path; - const workdir = path.join(path.sep, "home", "user", "project"); - const tempDir = path.join(workdir, "supabase", ".temp"); - const paths = legacyProjectStatePaths(path, workdir); - - expect(paths.tempDir).toBe(tempDir); - expect(paths.projectRef).toBe(path.join(tempDir, "project-ref")); - expect(paths.poolerUrl).toBe(path.join(tempDir, "pooler-url")); - expect(paths.postgresVersion).toBe(path.join(tempDir, "postgres-version")); - expect(paths.restVersion).toBe(path.join(tempDir, "rest-version")); - expect(paths.gotrueVersion).toBe(path.join(tempDir, "gotrue-version")); - expect(paths.storageVersion).toBe(path.join(tempDir, "storage-version")); - expect(paths.storageMigration).toBe(path.join(tempDir, "storage-migration")); - expect(paths.linkedProjectCache).toBe(path.join(tempDir, "linked-project.json")); - expect(paths.cliLatest).toBe(path.join(tempDir, "cli-latest")); - expect(paths.pgdeltaDir).toBe(path.join(tempDir, "pgdelta")); - expect(paths.startSecretsDir).toBe(path.join(tempDir, "start-secrets")); - }).pipe(Effect.provide(BunServices.layer)), - ); - - it.effect("every temp path is nested under tempDir", () => - Effect.gen(function* () { - const path = yield* Path.Path; - const paths = legacyProjectStatePaths(path, "/tmp/wd"); - const { tempDir, ...rest } = paths; - for (const value of Object.values(rest)) { - expect(path.dirname(value)).toBe(tempDir); - } - }).pipe(Effect.provide(BunServices.layer)), - ); -}); - -describe("LegacyProjectState file access", () => { - it.effect("writes a state file, reads it back, and removes the directory", () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const state = yield* LegacyProjectState; - const workdir = yield* fs.makeTempDirectoryScoped({ prefix: "legacy-state-" }); - - yield* state.write(workdir, "postgresVersion", "17.0.0"); - expect(yield* state.read(workdir, "postgresVersion")).toBe("17.0.0"); - expect(yield* state.exists(workdir, "postgresVersion")).toBe(true); - - yield* state.remove(workdir, "postgresVersion"); - expect(yield* state.exists(workdir, "postgresVersion")).toBe(false); - - yield* state.write(workdir, "restVersion", "v1"); - yield* state.removeAll(workdir); - expect(yield* fs.exists(state.paths(workdir).tempDir)).toBe(false); - }).pipe(Effect.scoped, Effect.provide(services)), - ); -}); - -describe("LegacyProjectState.readProjectRef", () => { - const workdir = Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const dir = yield* fs.makeTempDirectoryScoped({ prefix: "ref-" }); - const tempDir = path.join(dir, "supabase", ".temp"); - return { fs, dir, tempDir, refPath: path.join(tempDir, "project-ref") }; - }); - - it.effect("returns None when the project-ref file is absent (not linked)", () => - Effect.gen(function* () { - const { dir } = yield* workdir; - expect(Option.isNone(yield* readRef(dir))).toBe(true); - }).pipe(Effect.provide(services)), - ); - - it.effect("returns the trimmed ref when the file holds a value", () => - Effect.gen(function* () { - const { fs, dir, tempDir, refPath } = yield* workdir; - yield* fs.makeDirectory(tempDir, { recursive: true }); - yield* fs.writeFileString(refPath, ` ${REF}\n`); - expect(Option.getOrNull(yield* readRef(dir))).toBe(REF); - }).pipe(Effect.provide(services)), - ); - - it.effect("treats a blank project-ref file as None", () => - Effect.gen(function* () { - const { fs, dir, tempDir, refPath } = yield* workdir; - yield* fs.makeDirectory(tempDir, { recursive: true }); - yield* fs.writeFileString(refPath, " \n"); - expect(Option.isNone(yield* readRef(dir))).toBe(true); - }).pipe(Effect.provide(services)), - ); - - it.effect("fails with ProjectRefReadError when the ref path is unreadable", () => - Effect.gen(function* () { - // Seeding project-ref as a directory makes the read fail with EISDIR, which must surface - // rather than collapse to "unlinked". - const { fs, dir, refPath } = yield* workdir; - yield* fs.makeDirectory(refPath, { recursive: true }); - const error = yield* Effect.flip(readRef(dir)); - expect(error).toBeInstanceOf(ProjectRefReadError); - expect(error.message).toMatch(/^failed to load project ref: /); - }).pipe(Effect.provide(services)), - ); - - it("classifies an unreadable ref file as permission without an unrelated command", () => { - const result = classifyCliErrorActionability( - new ProjectRefReadError({ message: "failed to load project ref: permission denied" }), - ); - expect(result.error_kind).toBe("user_actionable"); - expect(result.error_category).toBe("permission"); - expect(result.has_suggestion).toBe(false); - expect(result.suggestion_type).toBe("none"); - expect(result.suggested_command).toBeUndefined(); - }); -}); diff --git a/apps/cli/src/command-internal/link-services-core.integration.test.ts b/apps/cli/src/command-internal/link-services-core.integration.test.ts index 34bce477c1..4e67cc3ce8 100644 --- a/apps/cli/src/command-internal/link-services-core.integration.test.ts +++ b/apps/cli/src/command-internal/link-services-core.integration.test.ts @@ -1,4 +1,4 @@ -import { legacyProjectStateLayer } from "./legacy-project-state.layer.ts"; +import { legacyProjectStateLayer } from "../shared/config/legacy-project-state.layer.ts"; import { BunServices } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; import { Effect, FileSystem, Layer } from "effect"; diff --git a/apps/cli/src/command-internal/link-services-core.ts b/apps/cli/src/command-internal/link-services-core.ts index 36164b4491..7cbd5e28b6 100644 --- a/apps/cli/src/command-internal/link-services-core.ts +++ b/apps/cli/src/command-internal/link-services-core.ts @@ -3,7 +3,7 @@ import { Effect, Option } from "effect"; import { CommandSettings } from "../config/command-settings.service.ts"; import { CommandPlatformApi } from "../auth/command-platform-api.service.ts"; -import { LegacyProjectState } from "./legacy-project-state.service.ts"; +import { LegacyProjectState } from "../shared/config/legacy-project-state.service.ts"; import { fetchGotrueVersion, fetchPostgrestVersion, diff --git a/apps/cli/src/command-internal/linked-state.ts b/apps/cli/src/command-internal/linked-state.ts index 83dc1ce133..7c824d0d96 100644 --- a/apps/cli/src/command-internal/linked-state.ts +++ b/apps/cli/src/command-internal/linked-state.ts @@ -1,10 +1,10 @@ import { Effect, FileSystem, Option, Path } from "effect"; import { CommandSettings } from "../config/command-settings.service.ts"; -import { PROJECT_REF_PATTERN } from "../config/remote-target.service.ts"; +import { PROJECT_REF_PATTERN } from "../config/project-ref.service.ts"; import { findBranchName } from "./branch-target.ts"; import { formatNamedRef, sanitizeInlineName } from "./http-errors.ts"; -import { type LinkSnapshot, readLinkSnapshotSoft } from "./link-snapshot.ts"; +import { type LinkSnapshot, readLinkSnapshotSoft } from "../shared/config/link-snapshot.ts"; /** * Discriminated linked-state result. @@ -32,7 +32,7 @@ export type LinkedState = /** * Soft lookup of the currently linked ref: env `SUPABASE_PROJECT_ID`, else the checkout's link. - * Never prompts or fails, and does not depend on `RemoteTarget`, so runtimes like `status` can + * Never prompts or fails, and does not depend on `ProjectRefResolver`, so runtimes like `status` can * use it without wiring that resolver up. * * Candidates must match `PROJECT_REF_PATTERN`: the value is echoed verbatim by `status -o json` @@ -117,7 +117,7 @@ export const resolveLinkedState = Effect.fn("LinkedState.resolve")(function* () } // No cache at all: render the plain shape with zero API calls. Querying - // `RemoteTarget.parent` here would only match the ref's own default + // the linked parent here would only match the ref's own default // branch (misrendering it as "a branch of itself") or 403 when it // genuinely is a branch — there's no positive-confirmation case to attempt. return { linked: true, projectRef: linkedRef.value } as const; @@ -188,7 +188,7 @@ export function formatLinkedStateBlock(state: LinkedState): string { * what signals "not linked" for these formats. A degraded branch-linked * state still emits every field it knows; only `linked_branch` is absent. */ -export function linkedStateGoFields(state: LinkedState): Readonly> { +export function linkedStateFields(state: LinkedState): Readonly> { if (!state.linked) return {}; return { linked_project_ref: state.projectRef, @@ -202,7 +202,7 @@ export function linkedStateGoFields(state: LinkedState): Readonly { @@ -164,8 +150,8 @@ export class InvalidJwtSecretError extends Data.Error<{ readonly message: string const MIN_JWT_SECRET_LENGTH = 16; /** - * A failure that is not one of this module's config errors (a malformed uint override, a - * signing-key error), keeping the original error's name and message and no actionability. + * A failure that is not one of this module's config errors (a signing-key error, an unsupported + * algorithm), keeping the original error's name and message and no actionability. */ class LocalConfigValuesError extends Data.Error<{ readonly message: string }> { constructor(message: string, name = "Error") { @@ -176,492 +162,106 @@ class LocalConfigValuesError extends Data.Error<{ readonly message: string }> { } const thrownError = (cause: unknown) => - cause instanceof Error - ? new LocalConfigValuesError(cause.message, cause.name) - : new LocalConfigValuesError(String(cause)); - -/** - * Raised by {@link envOverridePort} when a `SUPABASE_*_PORT` env/dotenv override doesn't - * parse as a valid port. A malformed port override always hard-fails config loading; there's - * no path that reaches `status`/`stop` with one. - */ -export class InvalidPortEnvOverrideError extends Data.Error<{ readonly message: string }> { - static readonly [ErrorActionabilityFingerprintId] = "InvalidPortEnvOverrideError"; - constructor(dottedFieldPath: string, value: string) { - super({ message: `Invalid config for ${dottedFieldPath}: cannot parse "${value}" as a port` }); - this.name = "InvalidPortEnvOverrideError"; - } - get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { - return actionability.invalidConfig; - } -} - -/** Maximum valid port number. */ -const MAX_PORT = 65535; - -/** - * Port-flavored sibling of {@link envOverride}/{@link envOverrideBool}. Unlike the boolean - * sibling, which falls back to the configured value on a malformed override, a bad port - * override fails with {@link InvalidPortEnvOverrideError} instead. - */ -export const envOverridePort = Effect.fnUntraced(function* ( - name: string, - configuredPort: number, - dottedFieldPath: string, - projectEnvValues: Readonly> | undefined, -) { - const value = yield* envOverride(name, undefined, projectEnvValues); - if (value === undefined) return configuredPort; - const parsed = parseGoBaseZeroUint(value); - if (parsed === undefined || parsed > BigInt(MAX_PORT)) { - return yield* new InvalidPortEnvOverrideError(dottedFieldPath, value); - } - return Number(parsed); -}); - -/** `projectEnvValues` first, then the ambient environment. */ -const lookupEnv = Effect.fnUntraced(function* ( - name: string, - projectEnvValues: Readonly> | undefined, -) { - return projectEnvValues?.[name] ?? (yield* shellEnv(name)); -}); - -/** - * Resolves a `SUPABASE_` override for a config field: checks `projectEnvValues` - * (the project's already-resolved dotenv values) before falling back to the ambient `Config` - * environment, treating an empty value as unset. - * - * The resolved value can itself be an `env(VAR)` indirection (e.g. `env(API_ENABLED)`), - * resolved with the same precedence; an unresolved indirection is returned as-is. - */ -export const envOverride = Effect.fnUntraced(function* ( - name: string, - configured: Configured, - projectEnvValues: Readonly> | undefined, -) { - const value = yield* lookupEnv(name, projectEnvValues); - if (value === undefined || value.length === 0) return configured; - const indirection = ENV_CAPTURE_REGEX.exec(value)?.[1]; - if (indirection === undefined) return value; - const resolved = yield* lookupEnv(indirection, projectEnvValues); - return resolved !== undefined && resolved.length > 0 ? resolved : value; -}); - -/** - * Raised by {@link envOverrideBool} when a `SUPABASE_*_ENABLED` (or other bool-typed) - * env/dotenv override doesn't parse as an accepted bool spelling. A malformed bool override - * always hard-fails config loading, same as {@link InvalidPortEnvOverrideError}. - */ -export class InvalidBoolEnvOverrideError extends Data.Error<{ readonly message: string }> { - static readonly [ErrorActionabilityFingerprintId] = "InvalidBoolEnvOverrideError"; - constructor(dottedFieldPath: string, value: string) { - super({ message: `Invalid config for ${dottedFieldPath}: cannot parse "${value}" as a bool` }); - this.name = "InvalidBoolEnvOverrideError"; - } - get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { - return actionability.invalidConfig; - } -} - -/** - * Boolean-flavored sibling of {@link envOverride} for `SUPABASE_*` fields decoded as a - * native bool (`api.tls.enabled`, `auth.enabled`, and other `
.enabled` gates). A - * malformed override fails with {@link InvalidBoolEnvOverrideError} rather than falling back - * to `configured`. - */ -export const envOverrideBool = Effect.fnUntraced(function* ( - name: string, - configured: boolean, - dottedFieldPath: string, - projectEnvValues: Readonly> | undefined, -) { - const value = yield* envOverride(name, undefined, projectEnvValues); - if (value === undefined) return configured; - const parsed = parseGoBool(value); - if (parsed === undefined) { - return yield* new InvalidBoolEnvOverrideError(dottedFieldPath, value); - } - return parsed; -}); - -/** Raised by {@link envOverrideAnalyticsBackend} when `SUPABASE_ANALYTICS_BACKEND` isn't `"postgres"` or `"bigquery"`. */ -export class InvalidAnalyticsBackendEnvOverrideError extends Data.Error<{ - readonly message: string; -}> { - static readonly [ErrorActionabilityFingerprintId] = "InvalidAnalyticsBackendEnvOverrideError"; - constructor(dottedFieldPath: string, value: string) { - super({ - message: `Invalid config for ${dottedFieldPath}: cannot parse "${value}" as one of "postgres", "bigquery"`, - }); - this.name = "InvalidAnalyticsBackendEnvOverrideError"; - } - get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { - return actionability.invalidConfig; - } -} - -/** - * Validates the override-or-configured value against the closed `postgres`/`bigquery` set, - * checking both sources with a single check so the return type narrows correctly. - * `skipEnvOverride` lets a matched remote-config value win over a conflicting - * `SUPABASE_ANALYTICS_BACKEND`. - */ -export const envOverrideAnalyticsBackend = Effect.fnUntraced(function* ( - configured: string, - projectEnvValues: Readonly> | undefined, - skipEnvOverride = false, -) { - const value = skipEnvOverride - ? configured - : ((yield* envOverride("SUPABASE_ANALYTICS_BACKEND", undefined, projectEnvValues)) ?? - configured); - if (value !== "postgres" && value !== "bigquery") { - return yield* new InvalidAnalyticsBackendEnvOverrideError("analytics.backend", value); - } - return value; -}); - -/** Raised by {@link envOverrideRealtimeIpVersion} when `SUPABASE_REALTIME_IP_VERSION` isn't `"IPv4"` or `"IPv6"`. */ -export class InvalidRealtimeIpVersionEnvOverrideError extends Data.Error<{ - readonly message: string; -}> { - static readonly [ErrorActionabilityFingerprintId] = "InvalidRealtimeIpVersionEnvOverrideError"; - constructor(dottedFieldPath: string, value: string) { - super({ - message: `Invalid config for ${dottedFieldPath}: cannot parse "${value}" as one of "IPv4", "IPv6"`, - }); - this.name = "InvalidRealtimeIpVersionEnvOverrideError"; - } - - get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { - return actionability.invalidConfig; - } -} - -export const envOverrideRealtimeIpVersion = Effect.fnUntraced(function* ( - configured: string, - projectEnvValues: Readonly> | undefined, -) { - const value = - (yield* envOverride("SUPABASE_REALTIME_IP_VERSION", undefined, projectEnvValues)) ?? configured; - if (value !== "IPv4" && value !== "IPv6") { - return yield* new InvalidRealtimeIpVersionEnvOverrideError("realtime.ip_version", value); - } - return value; -}); - -/** `SUPABASE_REALTIME_MAX_HEADER_LENGTH` — see {@link envOverrideUint}. */ -export const envOverrideRealtimeMaxHeaderLength = Effect.fnUntraced(function* ( - configured: number, - projectEnvValues: Readonly> | undefined, -) { - return yield* envOverrideUint( - "SUPABASE_REALTIME_MAX_HEADER_LENGTH", - "realtime.max_header_length", - configured, - projectEnvValues, - ); -}); - -/** `SUPABASE_API_MAX_ROWS` — see {@link envOverrideUint}. */ -export const envOverrideApiMaxRows = Effect.fnUntraced(function* ( - configured: number, - projectEnvValues: Readonly> | undefined, -) { - return yield* envOverrideUint( - "SUPABASE_API_MAX_ROWS", - "api.max_rows", - configured, - projectEnvValues, - ); -}); - -/** Raised by {@link envOverridePoolMode} when `SUPABASE_DB_POOLER_POOL_MODE` isn't `"transaction"` or `"session"`. */ -export class InvalidPoolModeEnvOverrideError extends Data.Error<{ readonly message: string }> { - static readonly [ErrorActionabilityFingerprintId] = "InvalidPoolModeEnvOverrideError"; - constructor(dottedFieldPath: string, value: string) { - super({ - message: `Invalid config for ${dottedFieldPath}: cannot parse "${value}" as one of "transaction", "session"`, - }); - this.name = "InvalidPoolModeEnvOverrideError"; - } + cause instanceof ConfigValidateError || + cause instanceof InvalidJwtSecretError || + cause instanceof CliConfigValueError + ? cause + : cause instanceof Error + ? new LocalConfigValuesError(cause.message, cause.name) + : new LocalConfigValuesError(String(cause)); - get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { - return actionability.invalidConfig; - } -} +const attempt = (thunk: () => A) => Effect.try({ try: thunk, catch: thrownError }); -export const envOverridePoolMode = Effect.fnUntraced(function* ( +/** Narrows a configured string to one of `allowed`, failing with the codec wording otherwise. */ +export const narrowConfigEnum = ( + path: string, configured: string, - projectEnvValues: Readonly> | undefined, -) { - const value = - (yield* envOverride("SUPABASE_DB_POOLER_POOL_MODE", undefined, projectEnvValues)) ?? configured; - if (value !== "transaction" && value !== "session") { - return yield* new InvalidPoolModeEnvOverrideError("db.pooler.pool_mode", value); - } - return value; -}); - -/** Raised by {@link envOverrideEdgeRuntimePolicy} when `SUPABASE_EDGE_RUNTIME_POLICY` isn't `"per_worker"` or `"oneshot"`. */ -export class InvalidEdgeRuntimePolicyEnvOverrideError extends Data.Error<{ - readonly message: string; -}> { - static readonly [ErrorActionabilityFingerprintId] = "InvalidEdgeRuntimePolicyEnvOverrideError"; - constructor(dottedFieldPath: string, value: string) { - super({ - message: `Invalid config for ${dottedFieldPath}: cannot parse "${value}" as one of "per_worker", "oneshot"`, + allowed: ReadonlyArray, +): T => { + const match = allowed.find((candidate) => candidate === configured); + if (match === undefined) { + throw new CliConfigValueError({ + path, + tier: "config", + message: `Invalid config for ${path}: "${configured}" must be one of ${allowed + .map((value) => `"${value}"`) + .join(", ")}`, }); - this.name = "InvalidEdgeRuntimePolicyEnvOverrideError"; } + return match; +}; - get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { - return actionability.invalidConfig; - } +/** Narrows an unknown value to a plain object. */ +function asRecord(value: unknown): Record | undefined { + return typeof value === "object" && value !== null && !Array.isArray(value) + ? (value as Record) + : undefined; } -export const envOverrideEdgeRuntimePolicy = Effect.fnUntraced(function* ( - configured: string, - projectEnvValues: Readonly> | undefined, -) { - const value = - (yield* envOverride("SUPABASE_EDGE_RUNTIME_POLICY", undefined, projectEnvValues)) ?? configured; - if (value !== "per_worker" && value !== "oneshot") { - return yield* new InvalidEdgeRuntimePolicyEnvOverrideError("edge_runtime.policy", value); - } - return value; -}); - -/** `SUPABASE_DB_POOLER_DEFAULT_POOL_SIZE` — see {@link envOverrideUint}. */ -export const envOverrideDefaultPoolSize = Effect.fnUntraced(function* ( - configured: number, - projectEnvValues: Readonly> | undefined, -) { - return yield* envOverrideUint( - "SUPABASE_DB_POOLER_DEFAULT_POOL_SIZE", - "db.pooler.default_pool_size", - configured, - projectEnvValues, - ); -}); - -/** `SUPABASE_DB_POOLER_MAX_CLIENT_CONN` — see {@link envOverrideUint}. */ -export const envOverrideMaxClientConn = Effect.fnUntraced(function* ( - configured: number, - projectEnvValues: Readonly> | undefined, -) { - return yield* envOverrideUint( - "SUPABASE_DB_POOLER_MAX_CLIENT_CONN", - "db.pooler.max_client_conn", - configured, - projectEnvValues, - ); -}); - -/** - * Decrypts a resolved auth identity-key field when it's a dotenvx `encrypted:` value; an - * undecryptable value fails config loading rather than passing through unusable key material. - * Applied after {@link envOverride} so an env-sourced override is decrypted too, not just - * the config.toml value. - */ -export const decryptAuthSecret = Effect.fnUntraced(function* ( - value: string | undefined, - projectEnvValues: Readonly> | undefined, -) { - if (value === undefined || !isEncryptedSecret(value)) return value; - const dotenvPrivateKeys = collectDotenvPrivateKeys({ ...projectEnvValues, ...process.env }); - const decrypted = decryptSecret(value, dotenvPrivateKeys); - if (!decrypted.ok) { - return yield* new ConfigValidateError(`failed to parse config: ${decrypted.error}`); - } - return decrypted.value; -}); +const asString = (value: unknown): string | undefined => + typeof value === "string" ? value : undefined; /** * Resolves `[auth.email.smtp]`'s full field set, including a presence-based `enabled` * default `@supabase/config`'s schema can't express (it always decodes `enabled: false` - * when the key is absent from a present table). Exported so `start.handler.ts`'s GoTrue env - * resolution reuses this instead of re-deriving it. + * when the key is absent from a present table). */ -export const resolveAuthEmailSmtp = Effect.fnUntraced(function* ( +export function resolveAuthEmailSmtp( authDocument: Readonly> | undefined, - projectEnvValues: Readonly> | undefined, - /** - * Fields whose `envOverride`/`envOverrideBool`/`envOverridePort` calls below can fail on - * a malformed value (directly, or via {@link decryptAuthSecret}) belong in this set so a - * matched remote block's value wins instead of aborting on an unrelated bad env var. - * Defaults to empty for callers with no `[remotes.]` block to match against. - */ - remoteOverrideKeys: ReadonlySet = new Set(), -) { - const remoteWins = makeRemoteWins(remoteOverrideKeys); +): (SmtpInput & { readonly senderName: string | undefined }) | undefined { const smtpDoc = asRecord(asRecord(authDocument?.["email"])?.["smtp"]); if (smtpDoc === undefined) return undefined; return { - enabled: remoteWins("auth.email.smtp.enabled") - ? smtpDoc["enabled"] === undefined - ? true - : smtpDoc["enabled"] === true - : yield* envOverrideBool( - "SUPABASE_AUTH_EMAIL_SMTP_ENABLED", - smtpDoc["enabled"] === undefined ? true : smtpDoc["enabled"] === true, - "auth.email.smtp.enabled", - projectEnvValues, - ), - host: remoteWins("auth.email.smtp.host") - ? typeof smtpDoc["host"] === "string" - ? smtpDoc["host"] - : "" - : ((yield* envOverride( - "SUPABASE_AUTH_EMAIL_SMTP_HOST", - typeof smtpDoc["host"] === "string" ? smtpDoc["host"] : "", - projectEnvValues, - )) ?? ""), - port: remoteWins("auth.email.smtp.port") - ? typeof smtpDoc["port"] === "number" - ? smtpDoc["port"] - : 0 - : yield* envOverridePort( - "SUPABASE_AUTH_EMAIL_SMTP_PORT", - typeof smtpDoc["port"] === "number" ? smtpDoc["port"] : 0, - "auth.email.smtp.port", - projectEnvValues, - ), - user: remoteWins("auth.email.smtp.user") - ? typeof smtpDoc["user"] === "string" - ? smtpDoc["user"] - : "" - : ((yield* envOverride( - "SUPABASE_AUTH_EMAIL_SMTP_USER", - typeof smtpDoc["user"] === "string" ? smtpDoc["user"] : "", - projectEnvValues, - )) ?? ""), - // Decrypted like other secrets. `auth.email.smtp.pass` is in `ENV_OVERRIDABLE_KEYS` so a - // malformed ambient override can't abort decryption when a remote block already set it. - pass: remoteWins("auth.email.smtp.pass") - ? ((yield* decryptAuthSecret( - typeof smtpDoc["pass"] === "string" ? smtpDoc["pass"] : "", - projectEnvValues, - )) ?? "") - : ((yield* decryptAuthSecret( - (yield* envOverride( - "SUPABASE_AUTH_EMAIL_SMTP_PASS", - typeof smtpDoc["pass"] === "string" ? smtpDoc["pass"] : "", - projectEnvValues, - )) ?? "", - projectEnvValues, - )) ?? ""), - adminEmail: remoteWins("auth.email.smtp.admin_email") - ? typeof smtpDoc["admin_email"] === "string" - ? smtpDoc["admin_email"] - : "" - : ((yield* envOverride( - "SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL", - typeof smtpDoc["admin_email"] === "string" ? smtpDoc["admin_email"] : "", - projectEnvValues, - )) ?? ""), - senderName: remoteWins("auth.email.smtp.sender_name") - ? typeof smtpDoc["sender_name"] === "string" - ? smtpDoc["sender_name"] - : undefined - : yield* envOverride( - "SUPABASE_AUTH_EMAIL_SMTP_SENDER_NAME", - typeof smtpDoc["sender_name"] === "string" ? smtpDoc["sender_name"] : undefined, - projectEnvValues, - ), - } satisfies SmtpInput & { readonly senderName: string | undefined }; -}); + enabled: smtpDoc["enabled"] === undefined ? true : smtpDoc["enabled"] === true, + host: asString(smtpDoc["host"]) ?? "", + port: typeof smtpDoc["port"] === "number" ? smtpDoc["port"] : 0, + user: asString(smtpDoc["user"]) ?? "", + pass: asString(smtpDoc["pass"]) ?? "", + adminEmail: asString(smtpDoc["admin_email"]) ?? "", + senderName: asString(smtpDoc["sender_name"]), + }; +} /** - * `auth.captcha` validation requires both `provider` and `secret` when the section is - * enabled. Unlike `auth.passkey`/`auth.webauthn`, `config.auth.captcha` never decodes to - * `undefined` when `[auth.captcha]` is absent, so presence is read from the raw - * `authDocument` instead. Hoisted so `start.handler.ts`'s GoTrue env resolution shares this - * same resolved value. + * `config.auth.captcha` never decodes to `undefined` when `[auth.captcha]` is absent, so + * callers that need presence read the document; this only reshapes the effective value. */ -export const resolveAuthCaptcha = Effect.fnUntraced(function* ( - authDocument: Readonly> | undefined, +export function resolveAuthCaptcha( captcha: CliConfig["auth"]["captcha"], - projectEnvValues: Readonly> | undefined, - /** - * Fields whose `envOverride`/`envOverrideBool` calls below can fail on a malformed value - * (directly, or via {@link decryptAuthSecret}) belong in this set so a matched remote - * block's value wins instead of aborting on an unrelated bad env var. Defaults to empty for - * callers with no `[remotes.]` block to match against. - */ - remoteOverrideKeys: ReadonlySet = new Set(), -) { - const remoteWins = makeRemoteWins(remoteOverrideKeys); - const captchaDoc = asRecord(authDocument?.["captcha"]); +): CaptchaInput | undefined { return captcha - ? ({ - enabled: remoteWins("auth.captcha.enabled") - ? (captcha.enabled ?? false) - : captchaDoc !== undefined - ? yield* envOverrideBool( - "SUPABASE_AUTH_CAPTCHA_ENABLED", - captcha.enabled ?? false, - "auth.captcha.enabled", - projectEnvValues, - ) - : (captcha.enabled ?? false), - provider: remoteWins("auth.captcha.provider") - ? captcha.provider - : captchaDoc !== undefined - ? yield* envOverride( - "SUPABASE_AUTH_CAPTCHA_PROVIDER", - captcha.provider, - projectEnvValues, - ) - : captcha.provider, - // Decrypted like `auth.email.smtp.pass`. `auth.captcha.secret` is in - // `ENV_OVERRIDABLE_KEYS` so a malformed ambient override can't abort decryption when a - // remote block already set it. - secret: yield* decryptAuthSecret( - remoteWins("auth.captcha.secret") - ? captcha.secret - : captchaDoc !== undefined - ? yield* envOverride("SUPABASE_AUTH_CAPTCHA_SECRET", captcha.secret, projectEnvValues) - : captcha.secret, - projectEnvValues, - ), - } satisfies CaptchaInput) + ? { enabled: captcha.enabled ?? false, provider: captcha.provider, secret: captcha.secret } : undefined; -}); +} /** - * Resolves the signing secret from the already-resolved `auth.jwt_secret`: empty falls back - * to {@link defaultJwtSecret}, shorter than {@link MIN_JWT_SECRET_LENGTH} fails with + * Resolves the signing secret from the effective `auth.jwt_secret`: empty falls back to + * {@link defaultJwtSecret}, shorter than {@link MIN_JWT_SECRET_LENGTH} throws * {@link InvalidJwtSecretError}. */ -export const resolveJwtSecret = Effect.fnUntraced(function* (configured: string | undefined) { +export function resolveJwtSecret(configured: string | undefined): string { if (configured === undefined || configured.length === 0) return defaultJwtSecret; if (configured.length < MIN_JWT_SECRET_LENGTH) { - return yield* new InvalidJwtSecretError(); + throw new InvalidJwtSecretError(); } return configured; -}); +} function resolveOpaqueKey(configured: string | undefined, fallback: string): string { return configured !== undefined && configured.length > 0 ? configured : fallback; } -const resolveSignedKey = Effect.fnUntraced(function* ( +function resolveSignedKey( configured: string | undefined, jwtSecret: string, signingKey: Jwk | undefined, role: "anon" | "service_role", -) { +): string { if (configured !== undefined && configured.length > 0) return configured; - return yield* Effect.try({ - try: () => - signingKey !== undefined - ? generateAsymmetricGoJwt(signingKey, role) - : generateGoJwt(jwtSecret, role), - catch: thrownError, - }); -}); + return signingKey !== undefined + ? generateAsymmetricLocalJwt(signingKey, role) + : generateLocalJwt(jwtSecret, role); +} /** JWK fields, matching {@link Jwk}. */ const JwkSchema = Schema.Struct({ @@ -717,42 +317,19 @@ const readSigningKeysFile = Effect.fnUntraced(function* (workdir: string, signin })) satisfies ReadonlyArray; }); -/** See {@link readSigningKeysFile}. */ -const loadSigningKeys = Effect.fnUntraced(function* (workdir: string, signingKeysPath: string) { - return yield* readSigningKeysFile(workdir, signingKeysPath); -}); - /** * Returns the parsed signing keys only when auth is enabled and a path is configured; * `undefined` otherwise, so callers fall back to their own default key shape. Shared by * {@link resolveLocalJwks} and `start.handler.ts`'s `GOTRUE_JWT_KEYS` so both resolvers agree - * on which key(s) apply. `remoteOverrideKeys` lets a matched remote block win over a - * conflicting env override on `auth.enabled`/`auth.signing_keys_path`. + * on which key(s) apply. */ export const resolveConfiguredSigningKeys = Effect.fnUntraced(function* ( config: CliConfig, workdir: string, - projectEnvValues: Readonly> | undefined, - remoteOverrideKeys: ReadonlySet = new Set(), ) { - const remoteWins = makeRemoteWins(remoteOverrideKeys); - const authEnabled = remoteWins("auth.enabled") - ? config.auth.enabled - : yield* envOverrideBool( - "SUPABASE_AUTH_ENABLED", - config.auth.enabled, - "auth.enabled", - projectEnvValues, - ); - const signingKeysPath = remoteWins("auth.signing_keys_path") - ? config.auth.signing_keys_path - : yield* envOverride( - "SUPABASE_AUTH_SIGNING_KEYS_PATH", - config.auth.signing_keys_path, - projectEnvValues, - ); - return authEnabled && signingKeysPath !== undefined && signingKeysPath.length > 0 - ? yield* loadSigningKeys(workdir, signingKeysPath) + const signingKeysPath = config.auth.signing_keys_path; + return config.auth.enabled && signingKeysPath !== undefined && signingKeysPath.length > 0 + ? yield* readSigningKeysFile(workdir, signingKeysPath) : undefined; }); @@ -788,9 +365,8 @@ const readApiTlsFiles = Effect.fnUntraced(function* ( }); /** - * One `[auth.email.template.]` entry, already env-override-resolved. `subject` is - * `string | undefined` rather than a plain `string` — see {@link resolveAuthEmail}'s doc - * comment for why. + * One `[auth.email.template.]` entry. `subject` is `string | undefined` rather than a plain + * `string` — see {@link resolveAuthEmail} for why. */ interface ResolvedAuthEmailTemplate { readonly subject: string | undefined; @@ -817,145 +393,41 @@ export type ResolvedAuthEmail = Omit | undefined, - projectEnvValues: Readonly> | undefined, - /** - * Fields whose `envOverride`/`envOverrideBool` calls (here or in the caller-side file read - * for `content_path`) can fail on a malformed value belong in this set so a matched remote - * block's value wins instead of aborting on an unrelated bad env var. Covers top-level - * `auth.email.*` leaves as well as each `template..*`/`notification..*` leaf. - */ - remoteOverrideKeys: ReadonlySet = new Set(), -) { - const remoteWins = makeRemoteWins(remoteOverrideKeys); +): ResolvedAuthEmail { const emailDoc = asRecord(authDocument?.["email"]); const templateDoc = asRecord(emailDoc?.["template"]); const notificationDoc = asRecord(emailDoc?.["notification"]); const template: Record = {}; for (const [name, tmpl] of Object.entries(email.template)) { - const envPrefix = `SUPABASE_AUTH_EMAIL_TEMPLATE_${name.toUpperCase()}`; - const rawSubjectPresent = asRecord(templateDoc?.[name])?.["subject"] !== undefined; - const envSubject = remoteWins(`auth.email.template.${name}.subject`) - ? undefined - : yield* envOverride(`${envPrefix}_SUBJECT`, undefined, projectEnvValues); + const entry = asRecord(templateDoc?.[name]); template[name] = { - subject: envSubject ?? (rawSubjectPresent ? tmpl.subject : undefined), - content_path: remoteWins(`auth.email.template.${name}.content_path`) - ? tmpl.content_path - : ((yield* envOverride(`${envPrefix}_CONTENT_PATH`, tmpl.content_path, projectEnvValues)) ?? - tmpl.content_path), - // `content` counts as present when set via TOML or the `_CONTENT` env override; - // {@link readAuthEmailTemplateContent} rejects it below unless `content_path` is also set. - content_present: - asRecord(templateDoc?.[name])?.["content"] !== undefined || - (remoteWins(`auth.email.template.${name}.content`) - ? false - : (yield* envOverride(`${envPrefix}_CONTENT`, undefined, projectEnvValues)) !== - undefined), + subject: entry?.["subject"] !== undefined ? tmpl.subject : undefined, + content_path: tmpl.content_path, + content_present: entry?.["content"] !== undefined, }; } const notification: Record = {}; for (const [name, tmpl] of Object.entries(email.notification)) { - const envPrefix = `SUPABASE_AUTH_EMAIL_NOTIFICATION_${name.toUpperCase()}`; - const rawSubjectPresent = asRecord(notificationDoc?.[name])?.["subject"] !== undefined; - const envSubject = remoteWins(`auth.email.notification.${name}.subject`) - ? undefined - : yield* envOverride(`${envPrefix}_SUBJECT`, undefined, projectEnvValues); + const entry = asRecord(notificationDoc?.[name]); notification[name] = { - enabled: remoteWins(`auth.email.notification.${name}.enabled`) - ? tmpl.enabled - : yield* envOverrideBool( - `${envPrefix}_ENABLED`, - tmpl.enabled, - `auth.email.notification.${name}.enabled`, - projectEnvValues, - ), - subject: envSubject ?? (rawSubjectPresent ? tmpl.subject : undefined), - content_path: remoteWins(`auth.email.notification.${name}.content_path`) - ? tmpl.content_path - : ((yield* envOverride(`${envPrefix}_CONTENT_PATH`, tmpl.content_path, projectEnvValues)) ?? - tmpl.content_path), - // Same `_CONTENT` env-presence fold as the template loop above. - content_present: - asRecord(notificationDoc?.[name])?.["content"] !== undefined || - (remoteWins(`auth.email.notification.${name}.content`) - ? false - : (yield* envOverride(`${envPrefix}_CONTENT`, undefined, projectEnvValues)) !== - undefined), + enabled: tmpl.enabled, + subject: entry?.["subject"] !== undefined ? tmpl.subject : undefined, + content_path: tmpl.content_path, + content_present: entry?.["content"] !== undefined, }; } - return { - ...email, - enable_signup: remoteWins("auth.email.enable_signup") - ? email.enable_signup - : yield* envOverrideBool( - "SUPABASE_AUTH_EMAIL_ENABLE_SIGNUP", - email.enable_signup, - "auth.email.enable_signup", - projectEnvValues, - ), - double_confirm_changes: remoteWins("auth.email.double_confirm_changes") - ? email.double_confirm_changes - : yield* envOverrideBool( - "SUPABASE_AUTH_EMAIL_DOUBLE_CONFIRM_CHANGES", - email.double_confirm_changes, - "auth.email.double_confirm_changes", - projectEnvValues, - ), - enable_confirmations: remoteWins("auth.email.enable_confirmations") - ? email.enable_confirmations - : yield* envOverrideBool( - "SUPABASE_AUTH_EMAIL_ENABLE_CONFIRMATIONS", - email.enable_confirmations, - "auth.email.enable_confirmations", - projectEnvValues, - ), - secure_password_change: remoteWins("auth.email.secure_password_change") - ? email.secure_password_change - : yield* envOverrideBool( - "SUPABASE_AUTH_EMAIL_SECURE_PASSWORD_CHANGE", - email.secure_password_change, - "auth.email.secure_password_change", - projectEnvValues, - ), - max_frequency: remoteWins("auth.email.max_frequency") - ? email.max_frequency - : ((yield* envOverride( - "SUPABASE_AUTH_EMAIL_MAX_FREQUENCY", - email.max_frequency, - projectEnvValues, - )) ?? email.max_frequency), - otp_length: remoteWins("auth.email.otp_length") - ? email.otp_length - : yield* envOverrideUint( - "SUPABASE_AUTH_EMAIL_OTP_LENGTH", - "auth.email.otp_length", - email.otp_length, - projectEnvValues, - ), - otp_expiry: remoteWins("auth.email.otp_expiry") - ? email.otp_expiry - : yield* envOverrideUint( - "SUPABASE_AUTH_EMAIL_OTP_EXPIRY", - "auth.email.otp_expiry", - email.otp_expiry, - projectEnvValues, - ), - template, - notification, - } satisfies ResolvedAuthEmail; -}); + return { ...email, template, notification }; +} /** * Reads each template/notification's content file, run only when auth is enabled. Every @@ -1019,1253 +491,183 @@ const readAuthEmailTemplateContent = Effect.fnUntraced(function* ( } }); -// Every field routed through {@link envOverrideUint} is an unsigned 64-bit value; comparing -// as `BigInt` (not `Number`) avoids precision loss near `2^64`. -const UINT_MAX = 18446744073709551615n; // 2^64 - 1 - -/** - * Base-0 unsigned integer literal parsing (`0b`/`0o`/`0x` prefixes, and a bare leading zero - * also meaning octal — so `"010"` parses as `8`), matching Go's `strconv.ParseUint(str, 0, …)` - * grammar. Underscores between digits are allowed; a leading sign is never accepted. Returns - * `undefined` for anything invalid instead of throwing, leaving bit-width bounds to the caller. - */ -function parseGoBaseZeroUint(value: string): bigint | undefined { - if (value.length === 0 || value.startsWith("+") || value.startsWith("-")) return undefined; - - let literal: string | undefined; - if (/^0[bB](_?[01])+$/.test(value)) { - literal = `0b${value.slice(2).replaceAll("_", "")}`; - } else if (/^0[oO](_?[0-7])+$/.test(value)) { - literal = `0o${value.slice(2).replaceAll("_", "")}`; - } else if (/^0[xX](_?[0-9a-fA-F])+$/.test(value)) { - literal = `0x${value.slice(2).replaceAll("_", "")}`; - } else if (value.startsWith("0") && value.length > 1) { - // A bare leading zero is always octal, with no fallback to decimal — `"08"`/`"09"` are - // rejected, not read as decimal 8/9. - literal = /^[0-7](_?[0-7])*$/.test(value) ? `0o${value.replaceAll("_", "")}` : undefined; - } else { - literal = /^[0-9](_?[0-9])*$/.test(value) ? value.replaceAll("_", "") : undefined; - } - if (literal === undefined) return undefined; - - try { - // `BigInt` natively parses `0b`/`0o`/`0x`-prefixed literals in the corresponding base. - return BigInt(literal); - } catch { - return undefined; - } +/** The effective `db.settings`, never `undefined` so callers can read fields directly. */ +export function resolveDbSettingsEnvOverrides( + settings: CliConfig["db"]["settings"], +): NonNullable { + return settings ?? {}; } -/** - * `SUPABASE_` sibling of {@link envOverridePort} for uncapped `uint`-typed fields - * (`db.major_version`, `auth.jwt_expiry`, …). Parses with {@link parseGoBaseZeroUint} and - * folds an invalid or out-of-{@link UINT_MAX} override into the generic "Invalid " - * error message. - */ -export const envOverrideUint = Effect.fnUntraced(function* ( - name: string, - dottedFieldPath: string, - configured: number, - projectEnvValues: Readonly> | undefined, -) { - const value = yield* envOverride(name, undefined, projectEnvValues); - if (value === undefined) return configured; - const parsed = parseGoBaseZeroUint(value); - if (parsed === undefined || parsed > UINT_MAX) { - return yield* new LocalConfigValuesError( - `Failed reading config: Invalid ${dottedFieldPath}: ${value}.`, - ); - } - return Number(parsed); -}); - -/** `SUPABASE_DB_MAJOR_VERSION` — see {@link envOverrideUint}. */ -export const envOverrideMajorVersion = Effect.fnUntraced(function* ( - configured: number, - projectEnvValues: Readonly> | undefined, -) { - return yield* envOverrideUint( - "SUPABASE_DB_MAJOR_VERSION", - "db.major_version", - configured, - projectEnvValues, - ); -}); - -const ORIOLEDB_VERSION_ENV = "SUPABASE_DB_ORIOLEDB_VERSION"; -const DEPRECATED_ORIOLEDB_VERSION_ENV = "SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION"; - -/** - * Reads the `db.orioledb_version` override through `read`, falling back to the deprecated - * `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` alias while `SUPABASE_DB_ORIOLEDB_VERSION` is unset. - */ -export const readOrioleDBVersionEnvOverride = Effect.fnUntraced(function* ( - read: ( - name: typeof ORIOLEDB_VERSION_ENV | typeof DEPRECATED_ORIOLEDB_VERSION_ENV, - ) => Effect.Effect, -) { - return (yield* read(ORIOLEDB_VERSION_ENV)) ?? (yield* read(DEPRECATED_ORIOLEDB_VERSION_ENV)); -}); - -/** `SUPABASE_DB_ORIOLEDB_VERSION` or its deprecated alias — see {@link envOverride}. */ -export const envOverrideOrioleDBVersion = Effect.fnUntraced(function* ( - configured: string | undefined, - projectEnvValues: Readonly> | undefined, -) { - return ( - (yield* readOrioleDBVersionEnvOverride((name) => - envOverride(name, undefined, projectEnvValues), - )) ?? configured - ); -}); +/** Resolves `auth.external_url`, which the schema doesn't model, from the effective document. */ +export function resolveAuthExternalUrl( + document: Readonly> | undefined, +): string | undefined { + return asString(asRecord(document?.["auth"])?.["external_url"]); +} -/** - * Set by {@link withSingleOrioleDBVersionEnvWarning}; while unset, every config read that finds the - * deprecated OrioleDB env alias prints its WARN. - */ -const OrioleDBVersionEnvWarningPrinted = Context.Reference>>( - "supabase/cli/OrioleDBVersionEnvWarningPrinted", - { defaultValue: () => Option.none() }, -); - -/** Runs `effect` so its config reads print the deprecated OrioleDB env WARN at most once. */ -export const withSingleOrioleDBVersionEnvWarning = (effect: Effect.Effect) => - Effect.flatMap(Ref.make(false), (printed) => - Effect.provideService(effect, OrioleDBVersionEnvWarningPrinted, Option.some(printed)), - ); +/** Hook-type iteration order for {@link resolveAuthHooks}'s output. */ +const HOOK_TYPE_ORDER = [ + "mfa_verification_attempt", + "password_verification_attempt", + "custom_access_token", + "send_sms", + "send_email", + "before_user_created", +] as const; -/** - * The deprecation warning to print when `read` finds `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` set, - * unless an enclosing {@link withSingleOrioleDBVersionEnvWarning} already printed it. - */ -export const deprecatedOrioleDBVersionEnvWarning = Effect.fnUntraced(function* ( - read: (name: typeof DEPRECATED_ORIOLEDB_VERSION_ENV) => Effect.Effect, -) { - if ((yield* read(DEPRECATED_ORIOLEDB_VERSION_ENV)) === undefined) return undefined; - const printed = yield* OrioleDBVersionEnvWarningPrinted; - if (Option.isSome(printed) && (yield* Ref.getAndSet(printed.value, true))) return undefined; - return `WARN: ${DEPRECATED_ORIOLEDB_VERSION_ENV} is deprecated and will be removed in v3, use ${ORIOLEDB_VERSION_ENV} instead.`; -}); +/** camelCase key {@link resolveAuthHooks} exposes per {@link HOOK_TYPE_ORDER} entry, matching `gotrue.service.ts`'s env-input field names. */ +const HOOK_TYPE_TO_CAMEL = { + mfa_verification_attempt: "mfaVerificationAttempt", + password_verification_attempt: "passwordVerificationAttempt", + custom_access_token: "customAccessToken", + send_sms: "sendSms", + send_email: "sendEmail", + before_user_created: "beforeUserCreated", +} as const satisfies Record<(typeof HOOK_TYPE_ORDER)[number], string>; -/** `SUPABASE_EDGE_RUNTIME_DENO_VERSION` — see {@link envOverrideUint}. */ -export const envOverrideDenoVersion = Effect.fnUntraced(function* ( - configured: number, - projectEnvValues: Readonly> | undefined, -) { - return yield* envOverrideUint( - "SUPABASE_EDGE_RUNTIME_DENO_VERSION", - "edge_runtime.deno_version", - configured, - projectEnvValues, - ); -}); +interface ResolvedAuthHook { + readonly enabled: boolean; + readonly uri: string; + readonly secrets: string; +} -/** - * Optional-uint sibling of {@link envOverrideUint} for `db.settings.*` fields - * (`max_connections`, `max_wal_senders`, …) that are left unset, not defaulted, when - * absent from config.toml. `configured`/the return value stay `number | undefined` to - * preserve that state through an override miss. - */ -const envOverrideOptionalUint = Effect.fnUntraced(function* ( - name: string, - dottedFieldPath: string, - configured: number | undefined, - projectEnvValues: Readonly> | undefined, -) { - const value = yield* envOverride(name, undefined, projectEnvValues); - if (value === undefined) return configured; - const parsed = parseGoBaseZeroUint(value); - if (parsed === undefined || parsed > UINT_MAX) { - return yield* new LocalConfigValuesError( - `Failed reading config: Invalid ${dottedFieldPath}: ${value}.`, - ); - } - return Number(parsed); -}); +export type ResolvedAuthHooks = { + readonly [K in (typeof HOOK_TYPE_TO_CAMEL)[keyof typeof HOOK_TYPE_TO_CAMEL]]: ResolvedAuthHook; +}; -/** - * Optional-bool sibling of {@link envOverrideBool} for `db.settings.track_commit_timestamp`, - * the only bool field in this file left unset (not defaulted) when absent. - */ -const envOverrideOptionalBool = Effect.fnUntraced(function* ( - name: string, - configured: boolean | undefined, - dottedFieldPath: string, - projectEnvValues: Readonly> | undefined, -) { - const value = yield* envOverride(name, undefined, projectEnvValues); - if (value === undefined) return configured; - const parsed = parseGoBool(value); - if (parsed === undefined) { - return yield* new InvalidBoolEnvOverrideError(dottedFieldPath, value); +/** Reshapes the effective `auth.hook.` entries, with absent `uri`/`secrets` as `""`. */ +export function resolveAuthHooks(hook: CliConfig["auth"]["hook"]): ResolvedAuthHooks { + const result = {} as Record; + for (const hookType of HOOK_TYPE_ORDER) { + const h = hook[hookType]; + result[HOOK_TYPE_TO_CAMEL[hookType]] = { + enabled: h.enabled, + uri: h.uri ?? "", + secrets: h.secrets ?? "", + }; } - return parsed; -}); + return result as ResolvedAuthHooks; +} -/** Raised by {@link resolveDbSettingsEnvOverrides} when `SUPABASE_DB_SETTINGS_SESSION_REPLICATION_ROLE` isn't `"origin"`, `"replica"`, or `"local"`. */ -export class InvalidSessionReplicationRoleEnvOverrideError extends Data.Error<{ - readonly message: string; -}> { - static readonly [ErrorActionabilityFingerprintId] = - "InvalidSessionReplicationRoleEnvOverrideError"; - constructor(dottedFieldPath: string, value: string) { - super({ - message: `Invalid config for ${dottedFieldPath}: cannot parse "${value}" as one of "origin", "replica", "local"`, - }); - this.name = "InvalidSessionReplicationRoleEnvOverrideError"; - } +/** The effective `auth.mfa`. */ +export function resolveAuthMfa(mfa: CliConfig["auth"]["mfa"]): CliConfig["auth"]["mfa"] { + return mfa; +} - get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { - return actionability.invalidConfig; - } +/** The effective `auth.rate_limit`. */ +export function resolveGotrueRateLimit( + rateLimit: CliConfig["auth"]["rate_limit"], +): CliConfig["auth"]["rate_limit"] { + return rateLimit; } -/** - * Enum-flavored sibling of {@link envOverride} for `db.settings.session_replication_role`. - * Unlike {@link envOverrideAnalyticsBackend}, `configured` (and the return value) may - * be `undefined` — validation only runs once a value is actually present. - */ -const envOverrideSessionReplicationRole = Effect.fnUntraced(function* ( - configured: string | undefined, - projectEnvValues: Readonly> | undefined, -) { - const value = yield* envOverride( - "SUPABASE_DB_SETTINGS_SESSION_REPLICATION_ROLE", - configured, - projectEnvValues, - ); - if (value === undefined) return undefined; - if (value !== "origin" && value !== "replica" && value !== "local") { - return yield* new InvalidSessionReplicationRoleEnvOverrideError( - "db.settings.session_replication_role", - value, - ); - } - return value; -}); +/** The effective `auth.sessions`. */ +export function resolveGotrueSessions( + sessions: CliConfig["auth"]["sessions"], +): CliConfig["auth"]["sessions"] { + return sessions; +} /** - * Resolves every `db.settings.*` sub-field to its env-overridden value before - * `postgresSettingsToPostgresConfig` serializes `postgresql.conf`. `remoteOverrideKeys` - * lets a matched remote block's value win over a conflicting `SUPABASE_DB_SETTINGS_*` var, - * same as elsewhere in this file. + * Reads `auth.passkey`/`auth.webauthn`, which have no `@supabase/config` schema fields: presence + * and every field come from the effective document. An absent section stays absent. */ -export const resolveDbSettingsEnvOverrides = Effect.fnUntraced(function* ( - settings: CliConfig["db"]["settings"], - projectEnvValues: Readonly> | undefined, - remoteOverrideKeys: ReadonlySet = new Set(), -) { - const remoteWins = makeRemoteWins(remoteOverrideKeys); - return { - effective_cache_size: remoteWins("db.settings.effective_cache_size") - ? settings?.effective_cache_size - : yield* envOverride( - "SUPABASE_DB_SETTINGS_EFFECTIVE_CACHE_SIZE", - settings?.effective_cache_size, - projectEnvValues, - ), - logical_decoding_work_mem: remoteWins("db.settings.logical_decoding_work_mem") - ? settings?.logical_decoding_work_mem - : yield* envOverride( - "SUPABASE_DB_SETTINGS_LOGICAL_DECODING_WORK_MEM", - settings?.logical_decoding_work_mem, - projectEnvValues, - ), - maintenance_work_mem: remoteWins("db.settings.maintenance_work_mem") - ? settings?.maintenance_work_mem - : yield* envOverride( - "SUPABASE_DB_SETTINGS_MAINTENANCE_WORK_MEM", - settings?.maintenance_work_mem, - projectEnvValues, - ), - max_connections: remoteWins("db.settings.max_connections") - ? settings?.max_connections - : yield* envOverrideOptionalUint( - "SUPABASE_DB_SETTINGS_MAX_CONNECTIONS", - "db.settings.max_connections", - settings?.max_connections, - projectEnvValues, - ), - max_locks_per_transaction: remoteWins("db.settings.max_locks_per_transaction") - ? settings?.max_locks_per_transaction - : yield* envOverrideOptionalUint( - "SUPABASE_DB_SETTINGS_MAX_LOCKS_PER_TRANSACTION", - "db.settings.max_locks_per_transaction", - settings?.max_locks_per_transaction, - projectEnvValues, - ), - max_parallel_maintenance_workers: remoteWins("db.settings.max_parallel_maintenance_workers") - ? settings?.max_parallel_maintenance_workers - : yield* envOverrideOptionalUint( - "SUPABASE_DB_SETTINGS_MAX_PARALLEL_MAINTENANCE_WORKERS", - "db.settings.max_parallel_maintenance_workers", - settings?.max_parallel_maintenance_workers, - projectEnvValues, - ), - max_parallel_workers: remoteWins("db.settings.max_parallel_workers") - ? settings?.max_parallel_workers - : yield* envOverrideOptionalUint( - "SUPABASE_DB_SETTINGS_MAX_PARALLEL_WORKERS", - "db.settings.max_parallel_workers", - settings?.max_parallel_workers, - projectEnvValues, - ), - max_parallel_workers_per_gather: remoteWins("db.settings.max_parallel_workers_per_gather") - ? settings?.max_parallel_workers_per_gather - : yield* envOverrideOptionalUint( - "SUPABASE_DB_SETTINGS_MAX_PARALLEL_WORKERS_PER_GATHER", - "db.settings.max_parallel_workers_per_gather", - settings?.max_parallel_workers_per_gather, - projectEnvValues, - ), - max_replication_slots: remoteWins("db.settings.max_replication_slots") - ? settings?.max_replication_slots - : yield* envOverrideOptionalUint( - "SUPABASE_DB_SETTINGS_MAX_REPLICATION_SLOTS", - "db.settings.max_replication_slots", - settings?.max_replication_slots, - projectEnvValues, - ), - max_slot_wal_keep_size: remoteWins("db.settings.max_slot_wal_keep_size") - ? settings?.max_slot_wal_keep_size - : yield* envOverride( - "SUPABASE_DB_SETTINGS_MAX_SLOT_WAL_KEEP_SIZE", - settings?.max_slot_wal_keep_size, - projectEnvValues, - ), - max_standby_archive_delay: remoteWins("db.settings.max_standby_archive_delay") - ? settings?.max_standby_archive_delay - : yield* envOverride( - "SUPABASE_DB_SETTINGS_MAX_STANDBY_ARCHIVE_DELAY", - settings?.max_standby_archive_delay, - projectEnvValues, - ), - max_standby_streaming_delay: remoteWins("db.settings.max_standby_streaming_delay") - ? settings?.max_standby_streaming_delay - : yield* envOverride( - "SUPABASE_DB_SETTINGS_MAX_STANDBY_STREAMING_DELAY", - settings?.max_standby_streaming_delay, - projectEnvValues, - ), - max_wal_size: remoteWins("db.settings.max_wal_size") - ? settings?.max_wal_size - : yield* envOverride( - "SUPABASE_DB_SETTINGS_MAX_WAL_SIZE", - settings?.max_wal_size, - projectEnvValues, - ), - max_wal_senders: remoteWins("db.settings.max_wal_senders") - ? settings?.max_wal_senders - : yield* envOverrideOptionalUint( - "SUPABASE_DB_SETTINGS_MAX_WAL_SENDERS", - "db.settings.max_wal_senders", - settings?.max_wal_senders, - projectEnvValues, - ), - max_worker_processes: remoteWins("db.settings.max_worker_processes") - ? settings?.max_worker_processes - : yield* envOverrideOptionalUint( - "SUPABASE_DB_SETTINGS_MAX_WORKER_PROCESSES", - "db.settings.max_worker_processes", - settings?.max_worker_processes, - projectEnvValues, - ), - session_replication_role: remoteWins("db.settings.session_replication_role") - ? settings?.session_replication_role - : yield* envOverrideSessionReplicationRole( - settings?.session_replication_role, - projectEnvValues, - ), - shared_buffers: remoteWins("db.settings.shared_buffers") - ? settings?.shared_buffers - : yield* envOverride( - "SUPABASE_DB_SETTINGS_SHARED_BUFFERS", - settings?.shared_buffers, - projectEnvValues, - ), - statement_timeout: remoteWins("db.settings.statement_timeout") - ? settings?.statement_timeout - : yield* envOverride( - "SUPABASE_DB_SETTINGS_STATEMENT_TIMEOUT", - settings?.statement_timeout, - projectEnvValues, - ), - track_activity_query_size: remoteWins("db.settings.track_activity_query_size") - ? settings?.track_activity_query_size - : yield* envOverride( - "SUPABASE_DB_SETTINGS_TRACK_ACTIVITY_QUERY_SIZE", - settings?.track_activity_query_size, - projectEnvValues, - ), - track_commit_timestamp: remoteWins("db.settings.track_commit_timestamp") - ? settings?.track_commit_timestamp - : yield* envOverrideOptionalBool( - "SUPABASE_DB_SETTINGS_TRACK_COMMIT_TIMESTAMP", - settings?.track_commit_timestamp, - "db.settings.track_commit_timestamp", - projectEnvValues, - ), - wal_keep_size: remoteWins("db.settings.wal_keep_size") - ? settings?.wal_keep_size - : yield* envOverride( - "SUPABASE_DB_SETTINGS_WAL_KEEP_SIZE", - settings?.wal_keep_size, - projectEnvValues, - ), - wal_sender_timeout: remoteWins("db.settings.wal_sender_timeout") - ? settings?.wal_sender_timeout - : yield* envOverride( - "SUPABASE_DB_SETTINGS_WAL_SENDER_TIMEOUT", - settings?.wal_sender_timeout, - projectEnvValues, - ), - work_mem: remoteWins("db.settings.work_mem") - ? settings?.work_mem - : yield* envOverride("SUPABASE_DB_SETTINGS_WORK_MEM", settings?.work_mem, projectEnvValues), - } satisfies NonNullable; -}); - -/** `password_requirements` fixed enum (`@supabase/config`'s `packages/config/src/auth/index.ts`). */ -const PASSWORD_REQUIREMENTS_VALUES = new Set([ - "", - "letters_digits", - "lower_upper_letters_digits", - "lower_upper_letters_digits_symbols", -]); - -/** - * Enum-flavored sibling of {@link envOverride} for `auth.password_requirements`. Exported so - * `db start`'s own eager-validation battery can call it directly instead of duplicating the - * check. - */ -export const envOverrideAuthPasswordRequirements = Effect.fnUntraced(function* ( - configured: string, - projectEnvValues: Readonly> | undefined, -) { - const override = yield* envOverride( - "SUPABASE_AUTH_PASSWORD_REQUIREMENTS", - undefined, - projectEnvValues, - ); - if (override !== undefined && !PASSWORD_REQUIREMENTS_VALUES.has(override)) { - return yield* new LocalConfigValuesError( - `Failed reading config: Invalid auth.password_requirements: ${override}.`, - ); - } - return override ?? configured; -}); - -/** Narrows an unknown value to a plain object. */ -function asRecord(value: unknown): Record | undefined { - return typeof value === "object" && value !== null && !Array.isArray(value) - ? (value as Record) - : undefined; -} - -/** - * Resolves `auth.external_url`, not modeled in `@supabase/config`'s schema, from the raw - * document. Shared by `supabase start`'s long-running GoTrue container and `db start`'s - * one-shot auth migration job so both resolve the same value. `remoteOverrideKeys` lets a - * matched remote block win over a conflicting `SUPABASE_AUTH_EXTERNAL_URL`. - */ -export const resolveAuthExternalUrl = Effect.fnUntraced(function* ( - document: Readonly> | undefined, - projectEnvValues: Readonly> | undefined, - remoteOverrideKeys: ReadonlySet = new Set(), -) { - const remoteWins = makeRemoteWins(remoteOverrideKeys); - const rawAuthExternalUrl = asRecord(document?.["auth"])?.["external_url"]; - const configuredAuthExternalUrl = - typeof rawAuthExternalUrl === "string" ? rawAuthExternalUrl : undefined; - if (remoteWins("auth.external_url")) return configuredAuthExternalUrl; - return yield* envOverride( - "SUPABASE_AUTH_EXTERNAL_URL", - configuredAuthExternalUrl, - projectEnvValues, - ); -}); - -/** Hook-type iteration order for {@link resolveAuthHooks}'s output. */ -const HOOK_TYPE_ORDER = [ - "mfa_verification_attempt", - "password_verification_attempt", - "custom_access_token", - "send_sms", - "send_email", - "before_user_created", -] as const; - -/** camelCase key {@link resolveAuthHooks} exposes per {@link HOOK_TYPE_ORDER} entry, matching `gotrue.service.ts`'s env-input field names. */ -const HOOK_TYPE_TO_CAMEL = { - mfa_verification_attempt: "mfaVerificationAttempt", - password_verification_attempt: "passwordVerificationAttempt", - custom_access_token: "customAccessToken", - send_sms: "sendSms", - send_email: "sendEmail", - before_user_created: "beforeUserCreated", -} as const satisfies Record<(typeof HOOK_TYPE_ORDER)[number], string>; - -interface ResolvedAuthHook { - readonly enabled: boolean; - readonly uri: string; - readonly secrets: string; -} - -type ResolvedAuthHooks = { - readonly [K in (typeof HOOK_TYPE_TO_CAMEL)[keyof typeof HOOK_TYPE_TO_CAMEL]]: ResolvedAuthHook; -}; - -/** - * Resolves `auth.hook..*` overrides in {@link HOOK_TYPE_ORDER}. Each type's presence in - * the raw `authDocument` gates whether an env override applies, since the schema always - * decodes a `{ enabled: false }` default regardless of file presence. Hoisted so both - * `resolveLocalConfigValues` and `start.handler.ts`'s GoTrue env resolution share this same - * result. - */ -export const resolveAuthHooks = Effect.fnUntraced(function* ( - authDocument: Readonly> | undefined, - hook: CliConfig["auth"]["hook"], - projectEnvValues: Readonly> | undefined, - /** - * Fields whose overrides below can fail or otherwise cause a downstream rejection on a - * malformed value belong in this set so a matched remote block's value wins instead. A - * matched remote flattens the whole `auth.hook.` block, so every leaf - * (`enabled`/`uri`/`secrets`) is included, not just `enabled`. - */ - remoteOverrideKeys: ReadonlySet = new Set(), -) { - const remoteWins = makeRemoteWins(remoteOverrideKeys); - const hookDocument = asRecord(authDocument?.["hook"]); - const result = {} as Record; - for (const hookType of HOOK_TYPE_ORDER) { - const h = hook[hookType]; - const hookSectionPresent = asRecord(hookDocument?.[hookType]) !== undefined; - const envPrefix = `SUPABASE_AUTH_HOOK_${hookType.toUpperCase()}`; - const enabled = remoteWins(`auth.hook.${hookType}.enabled`) - ? h.enabled - : hookSectionPresent - ? yield* envOverrideBool( - `${envPrefix}_ENABLED`, - h.enabled, - `auth.hook.${hookType}.enabled`, - projectEnvValues, - ) - : h.enabled; - const uri = - (remoteWins(`auth.hook.${hookType}.uri`) - ? h.uri - : hookSectionPresent - ? yield* envOverride(`${envPrefix}_URI`, h.uri, projectEnvValues) - : h.uri) ?? ""; - const secrets = - (remoteWins(`auth.hook.${hookType}.secrets`) - ? h.secrets - : hookSectionPresent - ? yield* envOverride(`${envPrefix}_SECRETS`, h.secrets, projectEnvValues) - : h.secrets) ?? ""; - result[HOOK_TYPE_TO_CAMEL[hookType]] = { enabled, uri, secrets }; - } - return result as ResolvedAuthHooks; -}); - -/** - * Resolves `auth.mfa`'s per-factor fields. Unlike hooks/smtp, `auth.mfa.` is always - * bound (no schema presence gap), so overrides always apply. Hoisted so both - * `resolveLocalConfigValues` and `start.handler.ts`'s GoTrue env resolution share this same - * result. - */ -export const resolveAuthMfa = Effect.fnUntraced(function* ( - mfa: CliConfig["auth"]["mfa"], - projectEnvValues: Readonly> | undefined, - /** - * Fields whose `envOverrideBool`/`envOverrideUint`/`envOverride` calls below can fail or - * otherwise cause a downstream rejection on a malformed value belong in this set so a - * matched remote block's value wins instead of aborting on an unrelated bad env var. - */ - remoteOverrideKeys: ReadonlySet = new Set(), -) { - const remoteWins = makeRemoteWins(remoteOverrideKeys); - return { - totp: { - enroll_enabled: remoteWins("auth.mfa.totp.enroll_enabled") - ? mfa.totp.enroll_enabled - : yield* envOverrideBool( - "SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED", - mfa.totp.enroll_enabled, - "auth.mfa.totp.enroll_enabled", - projectEnvValues, - ), - verify_enabled: remoteWins("auth.mfa.totp.verify_enabled") - ? mfa.totp.verify_enabled - : yield* envOverrideBool( - "SUPABASE_AUTH_MFA_TOTP_VERIFY_ENABLED", - mfa.totp.verify_enabled, - "auth.mfa.totp.verify_enabled", - projectEnvValues, - ), - }, - phone: { - enroll_enabled: remoteWins("auth.mfa.phone.enroll_enabled") - ? mfa.phone.enroll_enabled - : yield* envOverrideBool( - "SUPABASE_AUTH_MFA_PHONE_ENROLL_ENABLED", - mfa.phone.enroll_enabled, - "auth.mfa.phone.enroll_enabled", - projectEnvValues, - ), - verify_enabled: remoteWins("auth.mfa.phone.verify_enabled") - ? mfa.phone.verify_enabled - : yield* envOverrideBool( - "SUPABASE_AUTH_MFA_PHONE_VERIFY_ENABLED", - mfa.phone.verify_enabled, - "auth.mfa.phone.verify_enabled", - projectEnvValues, - ), - otp_length: remoteWins("auth.mfa.phone.otp_length") - ? mfa.phone.otp_length - : yield* envOverrideUint( - "SUPABASE_AUTH_MFA_PHONE_OTP_LENGTH", - "auth.mfa.phone.otp_length", - mfa.phone.otp_length, - projectEnvValues, - ), - template: remoteWins("auth.mfa.phone.template") - ? mfa.phone.template - : ((yield* envOverride( - "SUPABASE_AUTH_MFA_PHONE_TEMPLATE", - mfa.phone.template, - projectEnvValues, - )) ?? mfa.phone.template), - max_frequency: remoteWins("auth.mfa.phone.max_frequency") - ? mfa.phone.max_frequency - : ((yield* envOverride( - "SUPABASE_AUTH_MFA_PHONE_MAX_FREQUENCY", - mfa.phone.max_frequency, - projectEnvValues, - )) ?? mfa.phone.max_frequency), - }, - web_authn: { - enroll_enabled: remoteWins("auth.mfa.web_authn.enroll_enabled") - ? mfa.web_authn.enroll_enabled - : yield* envOverrideBool( - "SUPABASE_AUTH_MFA_WEB_AUTHN_ENROLL_ENABLED", - mfa.web_authn.enroll_enabled, - "auth.mfa.web_authn.enroll_enabled", - projectEnvValues, - ), - verify_enabled: remoteWins("auth.mfa.web_authn.verify_enabled") - ? mfa.web_authn.verify_enabled - : yield* envOverrideBool( - "SUPABASE_AUTH_MFA_WEB_AUTHN_VERIFY_ENABLED", - mfa.web_authn.verify_enabled, - "auth.mfa.web_authn.verify_enabled", - projectEnvValues, - ), - }, - max_enrolled_factors: remoteWins("auth.mfa.max_enrolled_factors") - ? mfa.max_enrolled_factors - : yield* envOverrideUint( - "SUPABASE_AUTH_MFA_MAX_ENROLLED_FACTORS", - "auth.mfa.max_enrolled_factors", - mfa.max_enrolled_factors, - projectEnvValues, - ), - } satisfies CliConfig["auth"]["mfa"]; -}); - -/** - * Resolves `auth.rate_limit.*`, all plain `uint`s with no presence gate: every - * `SUPABASE_AUTH_RATE_LIMIT_*` override applies unconditionally. - */ -export const resolveGotrueRateLimit = Effect.fnUntraced(function* ( - rateLimit: CliConfig["auth"]["rate_limit"], - projectEnvValues: Readonly> | undefined, -) { - return { - anonymous_users: yield* envOverrideUint( - "SUPABASE_AUTH_RATE_LIMIT_ANONYMOUS_USERS", - "auth.rate_limit.anonymous_users", - rateLimit.anonymous_users, - projectEnvValues, - ), - token_refresh: yield* envOverrideUint( - "SUPABASE_AUTH_RATE_LIMIT_TOKEN_REFRESH", - "auth.rate_limit.token_refresh", - rateLimit.token_refresh, - projectEnvValues, - ), - sign_in_sign_ups: yield* envOverrideUint( - "SUPABASE_AUTH_RATE_LIMIT_SIGN_IN_SIGN_UPS", - "auth.rate_limit.sign_in_sign_ups", - rateLimit.sign_in_sign_ups, - projectEnvValues, - ), - token_verifications: yield* envOverrideUint( - "SUPABASE_AUTH_RATE_LIMIT_TOKEN_VERIFICATIONS", - "auth.rate_limit.token_verifications", - rateLimit.token_verifications, - projectEnvValues, - ), - email_sent: yield* envOverrideUint( - "SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT", - "auth.rate_limit.email_sent", - rateLimit.email_sent, - projectEnvValues, - ), - sms_sent: yield* envOverrideUint( - "SUPABASE_AUTH_RATE_LIMIT_SMS_SENT", - "auth.rate_limit.sms_sent", - rateLimit.sms_sent, - projectEnvValues, - ), - web3: yield* envOverrideUint( - "SUPABASE_AUTH_RATE_LIMIT_WEB3", - "auth.rate_limit.web3", - rateLimit.web3, - projectEnvValues, - ), - } satisfies CliConfig["auth"]["rate_limit"]; -}); - -/** - * Resolves `auth.sessions.{timebox,inactivity_timeout}`. An env override can introduce a - * value even when `[auth.sessions]` was never in config.toml, since `config.auth.sessions` - * can be `undefined` here unlike other always-bound sections. - */ -export const resolveGotrueSessions = Effect.fnUntraced(function* ( - sessions: CliConfig["auth"]["sessions"], - projectEnvValues: Readonly> | undefined, -) { - const timebox = yield* envOverride( - "SUPABASE_AUTH_SESSIONS_TIMEBOX", - sessions?.timebox, - projectEnvValues, - ); - const inactivityTimeout = yield* envOverride( - "SUPABASE_AUTH_SESSIONS_INACTIVITY_TIMEOUT", - sessions?.inactivity_timeout, - projectEnvValues, - ); - if (timebox === undefined && inactivityTimeout === undefined) return sessions; - return { timebox, inactivity_timeout: inactivityTimeout } satisfies CliConfig["auth"]["sessions"]; -}); - -/** - * Resolves `auth.passkey`/`auth.webauthn`, which have no `@supabase/config` schema fields at - * all: presence and every field come from the raw TOML document. An absent section is never - * synthesized from an env override alone. - */ -export const resolveGotruePasskeyWebauthn = Effect.fnUntraced(function* ( +export function resolveGotruePasskeyWebauthn( document: Readonly> | undefined, - projectEnvValues: Readonly> | undefined, -) { +): { + readonly passkeyEnabled: boolean | undefined; + readonly webauthn: + | { + readonly rpId: string; + readonly rpDisplayName: string; + readonly rpOrigins: ReadonlyArray; + } + | undefined; +} { const authDoc = asRecord(document?.["auth"]); const passkeyDoc = asRecord(authDoc?.["passkey"]); const webauthnDoc = asRecord(authDoc?.["webauthn"]); const passkeyEnabled = passkeyDoc !== undefined - ? yield* envOverrideBool( - "SUPABASE_AUTH_PASSKEY_ENABLED", - yield* rawUnmodeledBool(passkeyDoc["enabled"], "auth.passkey.enabled"), - "auth.passkey.enabled", - projectEnvValues, - ) - : undefined; - const rpOriginsOverride = - webauthnDoc !== undefined - ? yield* envOverride("SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS", undefined, projectEnvValues) + ? rawUnmodeledBool(passkeyDoc["enabled"], "auth.passkey.enabled") : undefined; const webauthn = webauthnDoc !== undefined ? { - rpId: - (yield* envOverride( - "SUPABASE_AUTH_WEBAUTHN_RP_ID", - typeof webauthnDoc["rp_id"] === "string" ? webauthnDoc["rp_id"] : "", - projectEnvValues, - )) ?? "", - rpDisplayName: - (yield* envOverride( - "SUPABASE_AUTH_WEBAUTHN_RP_DISPLAY_NAME", - typeof webauthnDoc["rp_display_name"] === "string" - ? webauthnDoc["rp_display_name"] - : "", - projectEnvValues, - )) ?? "", - // A raw or `env(...)`-resolved `rp_origins` string is comma-split, not dropped. - rpOrigins: (() => { - if (rpOriginsOverride !== undefined) return strToArr(rpOriginsOverride); - const raw = webauthnDoc["rp_origins"]; - if (Array.isArray(raw)) { - return raw.filter((item): item is string => typeof item === "string"); - } - return typeof raw === "string" ? strToArr(raw) : []; - })(), + rpId: asString(webauthnDoc["rp_id"]) ?? "", + rpDisplayName: asString(webauthnDoc["rp_display_name"]) ?? "", + rpOrigins: rawOrigins(webauthnDoc["rp_origins"]) ?? [], } : undefined; return { passkeyEnabled, webauthn }; -}); +} -/** Resolves `auth.web3.*.enabled`, both value-typed with no presence gate — overrides always apply. */ -export const resolveGotrueWeb3 = Effect.fnUntraced(function* ( - web3: CliConfig["auth"]["web3"], - projectEnvValues: Readonly> | undefined, -) { - return { - solana: { - enabled: yield* envOverrideBool( - "SUPABASE_AUTH_WEB3_SOLANA_ENABLED", - web3.solana.enabled, - "auth.web3.solana.enabled", - projectEnvValues, - ), - }, - ethereum: { - enabled: yield* envOverrideBool( - "SUPABASE_AUTH_WEB3_ETHEREUM_ENABLED", - web3.ethereum.enabled, - "auth.web3.ethereum.enabled", - projectEnvValues, - ), - }, - } satisfies CliConfig["auth"]["web3"]; -}); +/** A raw or `env(...)`-resolved `rp_origins` string is comma-split, not dropped. */ +function rawOrigins(raw: unknown): Array | undefined { + if (Array.isArray(raw)) return raw.filter((item): item is string => typeof item === "string"); + return typeof raw === "string" ? strToArr(raw) : undefined; +} -/** Resolves `auth.oauth_server.*`, value-typed with no presence gate — overrides always apply. */ -export const resolveGotrueOAuthServer = Effect.fnUntraced(function* ( +/** The effective `auth.web3`. */ +export function resolveGotrueWeb3(web3: CliConfig["auth"]["web3"]): CliConfig["auth"]["web3"] { + return web3; +} + +/** The effective `auth.oauth_server`. */ +export function resolveGotrueOAuthServer( oauthServer: CliConfig["auth"]["oauth_server"], - projectEnvValues: Readonly> | undefined, -) { - return { - enabled: yield* envOverrideBool( - "SUPABASE_AUTH_OAUTH_SERVER_ENABLED", - oauthServer.enabled, - "auth.oauth_server.enabled", - projectEnvValues, - ), - authorization_url_path: - (yield* envOverride( - "SUPABASE_AUTH_OAUTH_SERVER_AUTHORIZATION_URL_PATH", - oauthServer.authorization_url_path, - projectEnvValues, - )) ?? oauthServer.authorization_url_path, - allow_dynamic_registration: yield* envOverrideBool( - "SUPABASE_AUTH_OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION", - oauthServer.allow_dynamic_registration, - "auth.oauth_server.allow_dynamic_registration", - projectEnvValues, - ), - } satisfies CliConfig["auth"]["oauth_server"]; -}); +): CliConfig["auth"]["oauth_server"] { + return oauthServer; +} -/** - * Resolves enabled `auth.third_party.` entries in a fixed order, forwarding only - * the enabled ones. `remoteOverrideKeys` lets a matched remote block's value win over a - * conflicting `SUPABASE_AUTH_THIRD_PARTY_*_ENABLED`, since `envOverrideBool` would otherwise - * fail and abort the whole call. - */ -export const resolveThirdPartyProviders = Effect.fnUntraced(function* ( +/** Lists enabled `auth.third_party.` entries in a fixed order, with their required fields. */ +export function resolveThirdPartyProviders( thirdParty: CliConfig["auth"]["third_party"], - projectEnvValues: Readonly> | undefined, - remoteOverrideKeys: ReadonlySet = new Set(), -) { - const remoteWins = makeRemoteWins(remoteOverrideKeys); +): ReadonlyArray { const resolved: Array = []; - if ( - remoteWins("auth.third_party.firebase.enabled") - ? thirdParty.firebase.enabled - : yield* envOverrideBool( - "SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED", - thirdParty.firebase.enabled, - "auth.third_party.firebase.enabled", - projectEnvValues, - ) - ) { - resolved.push({ - provider: "firebase", - requiredField: - (remoteWins("auth.third_party.firebase.project_id") - ? thirdParty.firebase.project_id - : yield* envOverride( - "SUPABASE_AUTH_THIRD_PARTY_FIREBASE_PROJECT_ID", - thirdParty.firebase.project_id, - projectEnvValues, - )) ?? "", - }); + if (thirdParty.firebase.enabled) { + resolved.push({ provider: "firebase", requiredField: thirdParty.firebase.project_id ?? "" }); } - if ( - remoteWins("auth.third_party.auth0.enabled") - ? thirdParty.auth0.enabled - : yield* envOverrideBool( - "SUPABASE_AUTH_THIRD_PARTY_AUTH0_ENABLED", - thirdParty.auth0.enabled, - "auth.third_party.auth0.enabled", - projectEnvValues, - ) - ) { - resolved.push({ - provider: "auth0", - requiredField: - (remoteWins("auth.third_party.auth0.tenant") - ? thirdParty.auth0.tenant - : yield* envOverride( - "SUPABASE_AUTH_THIRD_PARTY_AUTH0_TENANT", - thirdParty.auth0.tenant, - projectEnvValues, - )) ?? "", - }); + if (thirdParty.auth0.enabled) { + resolved.push({ provider: "auth0", requiredField: thirdParty.auth0.tenant ?? "" }); } - if ( - remoteWins("auth.third_party.aws_cognito.enabled") - ? thirdParty.aws_cognito.enabled - : yield* envOverrideBool( - "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_ENABLED", - thirdParty.aws_cognito.enabled, - "auth.third_party.aws_cognito.enabled", - projectEnvValues, - ) - ) { + if (thirdParty.aws_cognito.enabled) { resolved.push({ provider: "cognito", - requiredField: - (remoteWins("auth.third_party.aws_cognito.user_pool_id") - ? thirdParty.aws_cognito.user_pool_id - : yield* envOverride( - "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_USER_POOL_ID", - thirdParty.aws_cognito.user_pool_id, - projectEnvValues, - )) ?? "", - cognitoUserPoolRegion: remoteWins("auth.third_party.aws_cognito.user_pool_region") - ? thirdParty.aws_cognito.user_pool_region - : yield* envOverride( - "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_USER_POOL_REGION", - thirdParty.aws_cognito.user_pool_region, - projectEnvValues, - ), + requiredField: thirdParty.aws_cognito.user_pool_id ?? "", + cognitoUserPoolRegion: thirdParty.aws_cognito.user_pool_region, }); } - if ( - remoteWins("auth.third_party.clerk.enabled") - ? thirdParty.clerk.enabled - : yield* envOverrideBool( - "SUPABASE_AUTH_THIRD_PARTY_CLERK_ENABLED", - thirdParty.clerk.enabled, - "auth.third_party.clerk.enabled", - projectEnvValues, - ) - ) { - resolved.push({ - provider: "clerk", - requiredField: - (remoteWins("auth.third_party.clerk.domain") - ? thirdParty.clerk.domain - : yield* envOverride( - "SUPABASE_AUTH_THIRD_PARTY_CLERK_DOMAIN", - thirdParty.clerk.domain, - projectEnvValues, - )) ?? "", - }); + if (thirdParty.clerk.enabled) { + resolved.push({ provider: "clerk", requiredField: thirdParty.clerk.domain ?? "" }); } - if ( - remoteWins("auth.third_party.workos.enabled") - ? thirdParty.workos.enabled - : yield* envOverrideBool( - "SUPABASE_AUTH_THIRD_PARTY_WORKOS_ENABLED", - thirdParty.workos.enabled, - "auth.third_party.workos.enabled", - projectEnvValues, - ) - ) { - resolved.push({ - provider: "workos", - requiredField: - (remoteWins("auth.third_party.workos.issuer_url") - ? thirdParty.workos.issuer_url - : yield* envOverride( - "SUPABASE_AUTH_THIRD_PARTY_WORKOS_ISSUER_URL", - thirdParty.workos.issuer_url, - projectEnvValues, - )) ?? "", - }); + if (thirdParty.workos.enabled) { + resolved.push({ provider: "workos", requiredField: thirdParty.workos.issuer_url ?? "" }); } return resolved; -}); - -/** Fixed SMS provider priority — validation stops at the first enabled provider. */ -const SMS_PROVIDER_ORDER = [ - "twilio", - "twilio_verify", - "messagebird", - "textlocal", - "vonage", -] as const; +} /** - * Resolves `auth.sms`'s full field set so `SUPABASE_AUTH_SMS_*` overrides reach GoTrue's env, - * not just validation. Presence-gated per provider except `twilio`, whose default template - * always includes an uncommented `[auth.sms.twilio]` table; the 4 top-level scalars have no - * gate for the same reason. `test_otp` (a map) is left unresolved: it has no env var. + * The effective `auth.sms`. Phone signup is never enabled when no provider is configured to + * deliver an OTP, and twilio's two required ids default to `""`. */ -export const resolveAuthSms = Effect.fnUntraced(function* ( - authDocument: Readonly> | undefined, - sms: CliConfig["auth"]["sms"], - projectEnvValues: Readonly> | undefined, - /** - * Fields whose `envOverrideBool`/`decryptAuthSecret` calls below can fail on a malformed - * value belong in this set so a matched remote block's value wins instead of aborting on an - * unrelated bad env var. Reachable from the `db diff --linked`/`db pull` shadow path via - * `validateAuthSmsProviders`, not just this function's direct callers. - */ - remoteOverrideKeys: ReadonlySet = new Set(), -) { - const smsDoc = asRecord(authDocument?.["sms"]); - const remoteWins = makeRemoteWins(remoteOverrideKeys); - - function providerPresent(providerName: (typeof SMS_PROVIDER_ORDER)[number]): boolean { - // `twilio` is always considered present — see this function's doc comment. - if (providerName === "twilio") return true; - return smsDoc !== undefined && asRecord(smsDoc[providerName]) !== undefined; - } - - const resolveEnabled = Effect.fnUntraced(function* ( - providerName: (typeof SMS_PROVIDER_ORDER)[number], - configured: boolean, - ) { - if (remoteWins(`auth.sms.${providerName}.enabled`)) return configured; - if (!providerPresent(providerName)) return configured; - return yield* envOverrideBool( - `SUPABASE_AUTH_SMS_${providerName.toUpperCase()}_ENABLED`, - configured, - `auth.sms.${providerName}.enabled`, - projectEnvValues, - ); - }); - - // `remoteOverrideKey` is explicit (not reconstructed from `providerName`/`field`) since - // `field`'s shape differs per provider, and not every combination is a real config key. - const resolveField = Effect.fnUntraced(function* ( - providerName: (typeof SMS_PROVIDER_ORDER)[number], - field: string, - remoteOverrideKey: RemoteOverridableKey, - configured: string | undefined, - ) { - if (remoteWins(remoteOverrideKey)) return configured; - if (!providerPresent(providerName)) return configured; - return yield* envOverride( - `SUPABASE_AUTH_SMS_${providerName.toUpperCase()}_${field.toUpperCase()}`, - configured, - projectEnvValues, - ); - }); - - /** Resolves a provider's Secret-typed field, gated the same way `auth.email.smtp.pass` is. */ - const resolveSecretField = Effect.fnUntraced(function* ( - providerName: (typeof SMS_PROVIDER_ORDER)[number], - field: string, - remoteOverrideKey: RemoteOverridableKey, - configured: string | undefined, - ) { - return remoteWins(remoteOverrideKey) - ? yield* decryptAuthSecret(configured, projectEnvValues) - : yield* decryptAuthSecret( - yield* resolveField(providerName, field, remoteOverrideKey, configured), - projectEnvValues, - ); - }); - - const twilioEnabled = yield* resolveEnabled("twilio", sms.twilio.enabled); - const twilioVerifyEnabled = yield* resolveEnabled("twilio_verify", sms.twilio_verify.enabled); - const messagebirdEnabled = yield* resolveEnabled("messagebird", sms.messagebird.enabled); - const textlocalEnabled = yield* resolveEnabled("textlocal", sms.textlocal.enabled); - const vonageEnabled = yield* resolveEnabled("vonage", sms.vonage.enabled); +export function resolveAuthSms(sms: CliConfig["auth"]["sms"]): CliConfig["auth"]["sms"] { const anyProviderEnabled = - twilioEnabled || twilioVerifyEnabled || messagebirdEnabled || textlocalEnabled || vonageEnabled; - const enableSignupConfigured = remoteWins("auth.sms.enable_signup") - ? sms.enable_signup - : yield* envOverrideBool( - "SUPABASE_AUTH_SMS_ENABLE_SIGNUP", - sms.enable_signup, - "auth.sms.enable_signup", - projectEnvValues, - ); - + sms.twilio.enabled || + sms.twilio_verify.enabled || + sms.messagebird.enabled || + sms.textlocal.enabled || + sms.vonage.enabled; return { ...sms, - // Phone signup is never enabled when no provider is configured to deliver an OTP. - enable_signup: anyProviderEnabled ? enableSignupConfigured : false, - enable_confirmations: remoteWins("auth.sms.enable_confirmations") - ? sms.enable_confirmations - : yield* envOverrideBool( - "SUPABASE_AUTH_SMS_ENABLE_CONFIRMATIONS", - sms.enable_confirmations, - "auth.sms.enable_confirmations", - projectEnvValues, - ), - template: remoteWins("auth.sms.template") - ? sms.template - : ((yield* envOverride("SUPABASE_AUTH_SMS_TEMPLATE", sms.template, projectEnvValues)) ?? - sms.template), - max_frequency: remoteWins("auth.sms.max_frequency") - ? sms.max_frequency - : ((yield* envOverride( - "SUPABASE_AUTH_SMS_MAX_FREQUENCY", - sms.max_frequency, - projectEnvValues, - )) ?? sms.max_frequency), + enable_signup: anyProviderEnabled ? sms.enable_signup : false, twilio: { - enabled: twilioEnabled, - account_sid: - (yield* resolveField( - "twilio", - "account_sid", - "auth.sms.twilio.account_sid", - sms.twilio.account_sid, - )) ?? "", - message_service_sid: - (yield* resolveField( - "twilio", - "message_service_sid", - "auth.sms.twilio.message_service_sid", - sms.twilio.message_service_sid, - )) ?? "", - auth_token: yield* resolveSecretField( - "twilio", - "auth_token", - "auth.sms.twilio.auth_token", - sms.twilio.auth_token, - ), - }, - twilio_verify: { - enabled: twilioVerifyEnabled, - account_sid: yield* resolveField( - "twilio_verify", - "account_sid", - "auth.sms.twilio_verify.account_sid", - sms.twilio_verify.account_sid, - ), - message_service_sid: yield* resolveField( - "twilio_verify", - "message_service_sid", - "auth.sms.twilio_verify.message_service_sid", - sms.twilio_verify.message_service_sid, - ), - auth_token: yield* resolveSecretField( - "twilio_verify", - "auth_token", - "auth.sms.twilio_verify.auth_token", - sms.twilio_verify.auth_token, - ), - }, - messagebird: { - enabled: messagebirdEnabled, - originator: yield* resolveField( - "messagebird", - "originator", - "auth.sms.messagebird.originator", - sms.messagebird.originator, - ), - access_key: yield* resolveSecretField( - "messagebird", - "access_key", - "auth.sms.messagebird.access_key", - sms.messagebird.access_key, - ), - }, - textlocal: { - enabled: textlocalEnabled, - sender: yield* resolveField( - "textlocal", - "sender", - "auth.sms.textlocal.sender", - sms.textlocal.sender, - ), - api_key: yield* resolveSecretField( - "textlocal", - "api_key", - "auth.sms.textlocal.api_key", - sms.textlocal.api_key, - ), + ...sms.twilio, + account_sid: sms.twilio.account_sid ?? "", + message_service_sid: sms.twilio.message_service_sid ?? "", }, - vonage: { - enabled: vonageEnabled, - from: yield* resolveField("vonage", "from", "auth.sms.vonage.from", sms.vonage.from), - api_key: yield* resolveField( - "vonage", - "api_key", - "auth.sms.vonage.api_key", - sms.vonage.api_key, - ), - api_secret: yield* resolveSecretField( - "vonage", - "api_secret", - "auth.sms.vonage.api_secret", - sms.vonage.api_secret, - ), - }, - } satisfies CliConfig["auth"]["sms"]; -}); - -/** - * Validates only the first enabled provider in {@link SMS_PROVIDER_ORDER}; a later - * enabled-but-incomplete provider is never checked. Runs against {@link resolveAuthSms}'s - * env-override-aware result. - */ -const validateAuthSmsProviders = Effect.fnUntraced(function* ( - authDocument: Record | undefined, - sms: CliConfig["auth"]["sms"], - projectEnvValues: Readonly> | undefined, - remoteOverrideKeys: ReadonlySet = new Set(), -) { - const resolved = yield* resolveAuthSms(authDocument, sms, projectEnvValues, remoteOverrideKeys); - - const requireField = Effect.fnUntraced(function* ( - provider: string, - field: string, - value: string | undefined, - ) { - if (value === undefined || value.length === 0) { - return yield* new ConfigValidateError( - `Missing required field in config: auth.sms.${provider}.${field}`, - ); - } - }); - - if (resolved.twilio.enabled) { - yield* requireField("twilio", "account_sid", resolved.twilio.account_sid); - yield* requireField("twilio", "message_service_sid", resolved.twilio.message_service_sid); - yield* requireField("twilio", "auth_token", resolved.twilio.auth_token); - return; - } - if (resolved.twilio_verify.enabled) { - yield* requireField("twilio_verify", "account_sid", resolved.twilio_verify.account_sid); - yield* requireField( - "twilio_verify", - "message_service_sid", - resolved.twilio_verify.message_service_sid, - ); - yield* requireField("twilio_verify", "auth_token", resolved.twilio_verify.auth_token); - return; - } - if (resolved.messagebird.enabled) { - yield* requireField("messagebird", "originator", resolved.messagebird.originator); - yield* requireField("messagebird", "access_key", resolved.messagebird.access_key); - return; - } - if (resolved.textlocal.enabled) { - yield* requireField("textlocal", "sender", resolved.textlocal.sender); - yield* requireField("textlocal", "api_key", resolved.textlocal.api_key); - return; - } - if (resolved.vonage.enabled) { - yield* requireField("vonage", "from", resolved.vonage.from); - yield* requireField("vonage", "api_key", resolved.vonage.api_key); - yield* requireField("vonage", "api_secret", resolved.vonage.api_secret); - return; - } -}); - -/** Deleted external providers, warned on if still enabled, never validated. */ -const DEPRECATED_EXTERNAL_PROVIDERS = new Set(["linkedin", "slack"]); + }; +} /** Matches `GotrueExternalProviderInput`'s fields, kept separate to avoid a command-specific import. */ export interface ResolvedAuthExternalProvider { @@ -2279,560 +681,168 @@ export interface ResolvedAuthExternalProvider { } /** - * Weakly coerces an unmodeled raw-document value (no `@supabase/config` schema, e.g. custom + * Weakly coerces an unmodeled document value (no `@supabase/config` schema, e.g. custom * `auth.external` providers, `auth.passkey`/`auth.webauthn`) to a bool, since an `env(VAR)` * substitution there skips normal type coercion and leaves a literal `"true"`/`"false"` * string. A number coerces via truthiness (`!= 0`); an unparsable string or any other type - * fails rather than silently defaulting to `false`. + * throws rather than silently defaulting to `false`. */ -export const rawUnmodeledBool = Effect.fnUntraced(function* ( - value: unknown, - dottedFieldPath: string, -) { +export function rawUnmodeledBool(value: unknown, dottedFieldPath: string): boolean { if (value === undefined) return false; if (typeof value === "boolean") return value; if (typeof value === "number") return value !== 0; - if (typeof value === "string") { - const parsed = parseGoBool(value); - if (parsed === undefined) { - return yield* new InvalidBoolEnvOverrideError(dottedFieldPath, value); - } - return parsed; + const raw = typeof value === "string" ? value : String(value); + const parsed = typeof value === "string" ? parseBoolLiteral(value) : undefined; + if (parsed === undefined) { + throw new CliConfigValueError({ + path: dottedFieldPath, + tier: "config", + message: `Invalid config for ${dottedFieldPath}: "${raw}" is not a valid boolean`, + }); } - return yield* new InvalidBoolEnvOverrideError(dottedFieldPath, String(value)); -}); + return parsed; +} -/** - * Comma-splits a string into an array (empty string → `[]`), the same rule config decode - * applies to a raw or `env(VAR)`-resolved value destined for an unmodeled `[]string` field - * (e.g. `auth.webauthn.rp_origins`). - */ -export function strToArr(value: string): Array { +/** Comma-splits a string, with the empty string as `[]`. */ +function strToArr(value: string): Array { return value.length === 0 ? [] : value.split(","); } /** - * Resolves `auth.external.` overrides, iterating the raw document's provider names + * Resolves `auth.external.` entries, iterating the effective document's provider names * (not just the schema's fixed ~19) since custom providers decode with no schema at all. * `apple` is always included, since the default config.toml template registers it - * uncommented. Hoisted so both {@link validateAuthExternalProviders} and `start.handler.ts`'s - * GoTrue env resolution share this same result. + * uncommented. */ -export const resolveAuthExternalProviders = Effect.fnUntraced(function* ( +export function resolveAuthExternalProviders( authDocument: Readonly> | undefined, external: CliConfig["auth"]["external"], - projectEnvValues: Readonly> | undefined, - /** - * Fields whose `envOverrideBool`/`decryptAuthSecret` calls below can fail on a malformed - * value belong in this set so a matched remote block's value wins instead of aborting on an - * unrelated bad env var. Provider names are dynamic, so these keys are tracked via - * `applyRemoteOverride` rather than a fixed `ENV_OVERRIDABLE_KEYS` entry. - */ - remoteOverrideKeys: ReadonlySet = new Set(), -) { - const remoteWins = makeRemoteWins(remoteOverrideKeys); +): Record { const externalDoc = asRecord(authDocument?.["external"]); const result: Record = {}; const decodedProviders = new Map(Object.entries(external)); - // Iterates the raw document's keys (not `Object.entries(external)`) to catch custom - // provider names; `apple` is unioned in since the default template always registers it. const providerNames = new Set([...Object.keys(externalDoc ?? {}), "apple"]); for (const name of providerNames) { - if (DEPRECATED_EXTERNAL_PROVIDERS.has(name)) continue; - const envPrefix = `SUPABASE_AUTH_EXTERNAL_${name.toUpperCase()}`; const provider = decodedProviders.get(name); const rawProvider = provider === undefined ? asRecord(externalDoc?.[name]) : undefined; if (provider === undefined && rawProvider === undefined) continue; - const configuredEnabled = - provider?.enabled ?? - (yield* rawUnmodeledBool(rawProvider?.["enabled"], `auth.external.${name}.enabled`)); - const configuredClientId = - provider?.client_id ?? - (typeof rawProvider?.["client_id"] === "string" ? rawProvider["client_id"] : undefined); - const configuredSecret = - provider?.secret ?? - (typeof rawProvider?.["secret"] === "string" ? rawProvider["secret"] : undefined); - const configuredUrl = - provider?.url ?? (typeof rawProvider?.["url"] === "string" ? rawProvider["url"] : undefined); - const configuredRedirectUri = - provider?.redirect_uri ?? - (typeof rawProvider?.["redirect_uri"] === "string" ? rawProvider["redirect_uri"] : undefined); - const configuredSkipNonceCheck = - provider?.skip_nonce_check ?? - (yield* rawUnmodeledBool( - rawProvider?.["skip_nonce_check"], - `auth.external.${name}.skip_nonce_check`, - )); - const configuredEmailOptional = - provider?.email_optional ?? - (yield* rawUnmodeledBool( - rawProvider?.["email_optional"], - `auth.external.${name}.email_optional`, - )); - result[name] = { - enabled: remoteWins(`auth.external.${name}.enabled`) - ? configuredEnabled - : yield* envOverrideBool( - `${envPrefix}_ENABLED`, - configuredEnabled, - `auth.external.${name}.enabled`, - projectEnvValues, - ), - clientId: - (remoteWins(`auth.external.${name}.client_id`) - ? configuredClientId - : yield* envOverride(`${envPrefix}_CLIENT_ID`, configuredClientId, projectEnvValues)) ?? - "", - secret: remoteWins(`auth.external.${name}.secret`) - ? yield* decryptAuthSecret(configuredSecret, projectEnvValues) - : yield* decryptAuthSecret( - yield* envOverride(`${envPrefix}_SECRET`, configuredSecret, projectEnvValues), - projectEnvValues, - ), - url: - (remoteWins(`auth.external.${name}.url`) - ? configuredUrl - : yield* envOverride(`${envPrefix}_URL`, configuredUrl, projectEnvValues)) ?? "", - redirectUri: remoteWins(`auth.external.${name}.redirect_uri`) - ? configuredRedirectUri - : yield* envOverride(`${envPrefix}_REDIRECT_URI`, configuredRedirectUri, projectEnvValues), - skipNonceCheck: remoteWins(`auth.external.${name}.skip_nonce_check`) - ? configuredSkipNonceCheck - : yield* envOverrideBool( - `${envPrefix}_SKIP_NONCE_CHECK`, - configuredSkipNonceCheck, - `auth.external.${name}.skip_nonce_check`, - projectEnvValues, - ), - emailOptional: remoteWins(`auth.external.${name}.email_optional`) - ? configuredEmailOptional - : yield* envOverrideBool( - `${envPrefix}_EMAIL_OPTIONAL`, - configuredEmailOptional, - `auth.external.${name}.email_optional`, - projectEnvValues, - ), + enabled: + provider?.enabled ?? + rawUnmodeledBool(rawProvider?.["enabled"], `auth.external.${name}.enabled`), + clientId: provider?.client_id ?? asString(rawProvider?.["client_id"]) ?? "", + secret: provider?.secret ?? asString(rawProvider?.["secret"]), + url: provider?.url ?? asString(rawProvider?.["url"]) ?? "", + redirectUri: provider?.redirect_uri ?? asString(rawProvider?.["redirect_uri"]), + skipNonceCheck: + provider?.skip_nonce_check ?? + rawUnmodeledBool( + rawProvider?.["skip_nonce_check"], + `auth.external.${name}.skip_nonce_check`, + ), + emailOptional: + provider?.email_optional ?? + rawUnmodeledBool(rawProvider?.["email_optional"], `auth.external.${name}.email_optional`), }; } return result; -}); - -/** - * Validates required fields for every enabled `auth.external.` provider, including - * custom names `@supabase/config`'s schema silently drops at decode time. Runs against - * {@link resolveAuthExternalProviders}'s env-override-aware result, since the schema's own - * `requiredWhenEnabled` check only sees the pre-override, known-providers-only value. - */ -const validateAuthExternalProviders = Effect.fnUntraced(function* ( - authDocument: Record | undefined, - external: CliConfig["auth"]["external"], - projectEnvValues: Readonly> | undefined, - remoteOverrideKeys: ReadonlySet = new Set(), -) { - const resolved = yield* resolveAuthExternalProviders( - authDocument, - external, - projectEnvValues, - remoteOverrideKeys, - ); - for (const [name, provider] of Object.entries(resolved)) { - if (!provider.enabled) continue; - if (provider.clientId.length === 0) { - return yield* new ConfigValidateError( - `Missing required field in config: auth.external.${name}.client_id`, - ); - } - if ( - name !== "apple" && - name !== "google" && - (provider.secret === undefined || provider.secret.length === 0) - ) { - return yield* new ConfigValidateError( - `Missing required field in config: auth.external.${name}.secret`, - ); - } - } -}); +} /** - * Fails when `project_id` (post-override, post-workdir-basename-fallback) is an explicit - * empty string. Checked first: the sanitized workdir basename is merged in as a default - * before `config.toml`, so `project_id` is never empty by the time validation runs — a - * workdir whose basename sanitizes to `""` fails even with no `project_id` key at all. - * Fails with {@link InvalidJwtSecretError} when `auth.jwt_secret` is set but too short. - * Fails with {@link InvalidPortEnvOverrideError} when a `SUPABASE_*_PORT` env/dotenv - * override doesn't parse as a valid port. - * Fails with {@link InvalidBoolEnvOverrideError} when a `SUPABASE_*_ENABLED` env/dotenv - * override doesn't parse as a valid bool. - * Fails when a configured `api.tls` cert/key file can't be read — see + * @throws when `project_id` is an explicit empty string. Checked first: the sanitized workdir + * basename is the default, so a workdir whose basename sanitizes to `""` fails even with no + * `project_id` key at all. + * @throws {InvalidJwtSecretError} when `auth.jwt_secret` is set but too short. + * @throws when a configured `api.tls` cert/key file can't be read — see * {@link readApiTlsFiles}. - * Fails when `auth.signing_keys_path` is set, auth is enabled, and the file is missing, + * @throws when `auth.signing_keys_path` is set, auth is enabled, and the file is missing, * malformed, or its first key uses an unsupported algorithm — see - * {@link resolveConfiguredSigningKeys} and {@link generateAsymmetricGoJwt}. - * Fails when an email template's `content` is present without `content_path`, or a + * {@link resolveConfiguredSigningKeys} and {@link generateAsymmetricLocalJwt}. + * @throws when an email template's `content` is present without `content_path`, or a * configured `content_path` file can't be read — see {@link readAuthEmailTemplateContent}. - * Fails with {@link InvalidAnalyticsBackendEnvOverrideError} when `SUPABASE_ANALYTICS_BACKEND` - * doesn't parse as one of its accepted values. - * Fails with {@link ConfigValidateError} for every other validation branch, deferred to a - * single call to {@link validateResolvedConfig} at the end of this function. + * @throws {ConfigValidateError} for every other validation branch, deferred to a single call + * to {@link validateResolvedConfig} at the end of this function. */ export const resolveLocalConfigValues = Effect.fnUntraced(function* ( config: CliConfig, hostname: string, workdir: string, - projectEnvValues?: Readonly>, /** - * The raw, pre-schema-default TOML document `config` was decoded from, letting checks that - * hinge on section presence (not the always-defaulted decoded value) inspect the file - * directly. `undefined` callers simply skip those checks. + * The effective document `config` was decoded from, for checks that hinge on section presence + * (not the always-defaulted decoded value). `undefined` callers skip those checks. */ document?: Readonly>, - /** - * Config keys a matched `[remotes.]` block set at override tier. Every field whose - * `envOverride*` call (or a downstream {@link validateResolvedConfig} check) can fail on a - * malformed value belongs in this set — this function returns its whole object or fails, so - * an unconditional failure anywhere aborts the whole call, even for a field not part of the - * returned `LocalConfigValues`. Defaults to empty for callers with no `[remotes.]` block. - */ - remoteOverrideKeys: ReadonlySet = new Set(), - /** - * Default `project_id` supplied by `--project-ref`/the linked project ref before the - * basename fallback applies. `undefined` for `status`/`stop`, which have no such flag. - */ - projectIdFallback?: string, ) { - const path = yield* Path.Path; - const remoteWins = makeRemoteWins(remoteOverrideKeys); - // Checked first, before every other field: `config.project_id` is `undefined` only when the - // key is absent, in which case the sanitized workdir basename (or - // `projectIdFallback`) applies instead of a file value. - // - // Not gated by `remoteWins("project_id")` like the fields below: `envOverride` never turns - // an already non-empty remote-merged value empty or vice versa, so gating here couldn't - // change the accept/reject outcome. - const resolvedProjectId = yield* envOverride( - "SUPABASE_PROJECT_ID", - config.project_id ?? - (projectIdFallback !== undefined && projectIdFallback.length > 0 - ? projectIdFallback - : sanitizeProjectId(path.basename(workdir))), - projectEnvValues, - ); + const resolvedProjectId = config.project_id ?? ""; - const apiTlsEnabled = remoteWins("api.tls.enabled") - ? config.api.tls.enabled - : yield* envOverrideBool( - "SUPABASE_API_TLS_ENABLED", - config.api.tls.enabled, - "api.tls.enabled", - projectEnvValues, - ); - // TLS cert/key validation only runs while `api.enabled`, so this gates on the post-override - // value, not raw `config.api.enabled`. - const apiEnabled = remoteWins("api.enabled") - ? config.api.enabled - : yield* envOverrideBool( - "SUPABASE_API_ENABLED", - config.api.enabled, - "api.enabled", - projectEnvValues, - ); - const apiTlsCertPath = remoteWins("api.tls.cert_path") - ? config.api.tls.cert_path - : yield* envOverride("SUPABASE_API_TLS_CERT_PATH", config.api.tls.cert_path, projectEnvValues); - const apiTlsKeyPath = remoteWins("api.tls.key_path") - ? config.api.tls.key_path - : yield* envOverride("SUPABASE_API_TLS_KEY_PATH", config.api.tls.key_path, projectEnvValues); + const apiTlsEnabled = config.api.tls.enabled; + const apiEnabled = config.api.enabled; + const apiTlsCertPath = config.api.tls.cert_path; + const apiTlsKeyPath = config.api.tls.key_path; if (apiEnabled && apiTlsEnabled) { yield* readApiTlsFiles(workdir, apiTlsCertPath, apiTlsKeyPath); } - // `api.port === 0` is rejected only when `api.enabled`, unlike `db.port` below. Resolved once - // so the check and the URL derivation below share the same overridden value. - const apiPort = remoteWins("api.port") - ? config.api.port - : yield* envOverridePort("SUPABASE_API_PORT", config.api.port, "api.port", projectEnvValues); - const apiExternalUrl = resolveApiExternalUrl( - { - external_url: remoteWins("api.external_url") - ? config.api.external_url - : yield* envOverride( - "SUPABASE_API_EXTERNAL_URL", - config.api.external_url, - projectEnvValues, - ), - port: apiPort, - tls: { enabled: apiTlsEnabled }, - }, - hostname, + const apiPort = config.api.port; + const apiExternalUrl = yield* attempt(() => + resolveApiExternalUrl( + { + external_url: config.api.external_url, + port: apiPort, + tls: { enabled: apiTlsEnabled }, + }, + hostname, + ), ); - // Unlike `api.port`/`studio.port`/`local_smtp.port` below, `db.port` has no `enabled` gate — - // it's unconditionally required, and a decoded `0` fails validation. - const dbPort = remoteWins("db.port") - ? config.db.port - : yield* envOverridePort("SUPABASE_DB_PORT", config.db.port, "db.port", projectEnvValues); - // Validate-only: the shadow's own resolved `majorVersion` comes from - // `resolveDbBootstrapConfig`, which gates it separately. - const majorVersion = remoteWins("db.major_version") - ? config.db.major_version - : yield* envOverrideMajorVersion(config.db.major_version, projectEnvValues); - // Validate-only: the actual resolved settings `start` needs are recomputed at their own call - // site. - yield* resolveDbSettingsEnvOverrides(config.db.settings, projectEnvValues, remoteOverrideKeys); - // Validate-only: `start` doesn't otherwise consume `db.network_restrictions.enabled` (only - // `config push` does). - if (!remoteWins("db.network_restrictions.enabled")) { - yield* envOverrideBool( - "SUPABASE_DB_NETWORK_RESTRICTIONS_ENABLED", - config.db.network_restrictions.enabled, - "db.network_restrictions.enabled", - projectEnvValues, - ); - } - // `db.root_key` isn't modeled in `@supabase/config`'s schema, so it's read off the raw - // pre-schema document. The resolved, decrypted-if-`encrypted:` value is written verbatim into - // `/etc/postgresql-custom/pgsodium_root.key` on every start. - const rawRootKeyValue = asRecord(document?.["db"])?.["root_key"]; - if (rawRootKeyValue !== undefined && typeof rawRootKeyValue !== "string") { - return yield* new ConfigValidateError( - "failed to parse config: decoding failed due to the following error(s):\n\n'db.root_key' expected a map or struct", - ); + const dbPort = config.db.port; + const majorVersion = config.db.major_version; + // `db.root_key` isn't modeled in `@supabase/config`'s schema, so it's read off the document. + // The resolved value is written verbatim into `/etc/postgresql-custom/pgsodium_root.key`. + const rawRootKey = asRecord(document?.["db"])?.["root_key"]; + if (rawRootKey !== undefined && typeof rawRootKey !== "string") { + return yield* new ConfigValidateError("failed to parse config:\ndb.root_key: expected a table"); } - const rawRootKey = remoteWins("db.root_key") - ? rawRootKeyValue - : yield* envOverride("SUPABASE_DB_ROOT_KEY", rawRootKeyValue, projectEnvValues); const rootKey = - rawRootKey === undefined || rawRootKey.length === 0 - ? POSTGRES_DEFAULT_ROOT_KEY - : ((yield* decryptAuthSecret(rawRootKey, projectEnvValues)) ?? POSTGRES_DEFAULT_ROOT_KEY); + rawRootKey === undefined || rawRootKey.length === 0 ? POSTGRES_DEFAULT_ROOT_KEY : rawRootKey; const storageBucketNames = config.storage.buckets !== undefined ? Object.keys(config.storage.buckets) : []; - // `studio.port === 0` is rejected only when `studio.enabled`, same enabled-gated pattern as - // `api.port` above. - const studioEnabled = remoteWins("studio.enabled") - ? config.studio.enabled - : yield* envOverrideBool( - "SUPABASE_STUDIO_ENABLED", - config.studio.enabled, - "studio.enabled", - projectEnvValues, - ); - const studioPort = remoteWins("studio.port") - ? config.studio.port - : yield* envOverridePort( - "SUPABASE_STUDIO_PORT", - config.studio.port, - "studio.port", - projectEnvValues, - ); - // `envOverride` itself never fails, but `studio.api_url` feeds `validateResolvedConfig`'s - // URL-parse check below, which does throw on a malformed URL, so this is gated the same way - // as `studio.enabled`/`studio.port` above. - const studioApiUrl = remoteWins("studio.api_url") - ? config.studio.api_url - : ((yield* envOverride("SUPABASE_STUDIO_API_URL", config.studio.api_url, projectEnvValues)) ?? - config.studio.api_url); - // `local_smtp.port === 0` is rejected only when `local_smtp.enabled`; the deprecated - // `inbucket.enabled` alias is the same underlying flag, not a second one. - const mailpitEnabled = remoteWins("local_smtp.enabled") - ? config.local_smtp.enabled - : yield* envOverrideBool( - "SUPABASE_LOCAL_SMTP_ENABLED", - config.local_smtp.enabled, - "local_smtp.enabled", - projectEnvValues, - ); - const mailpitPort = remoteWins("local_smtp.port") - ? config.local_smtp.port - : yield* envOverridePort( - "SUPABASE_LOCAL_SMTP_PORT", - config.local_smtp.port, - "local_smtp.port", - projectEnvValues, - ); - const jwtSecret = yield* resolveJwtSecret( - yield* decryptAuthSecret( - remoteWins("auth.jwt_secret") - ? config.auth.jwt_secret - : yield* envOverride("SUPABASE_AUTH_JWT_SECRET", config.auth.jwt_secret, projectEnvValues), - projectEnvValues, - ), - ); - const signingKeysPath = remoteWins("auth.signing_keys_path") - ? config.auth.signing_keys_path - : yield* envOverride( - "SUPABASE_AUTH_SIGNING_KEYS_PATH", - config.auth.signing_keys_path, - projectEnvValues, - ); + const studioEnabled = config.studio.enabled; + const studioPort = config.studio.port; + const studioApiUrl = config.studio.api_url; + const mailpitEnabled = config.local_smtp.enabled; + const mailpitPort = config.local_smtp.port; + const jwtSecret = yield* attempt(() => resolveJwtSecret(config.auth.jwt_secret)); + const signingKeysPath = config.auth.signing_keys_path; // The signing-keys file read only runs when auth is enabled, so a disabled auth section // never opens/parses `signing_keys_path`, even a stale or missing one. JWT-secret validation // and anon/service_role key generation run unconditionally either way. - const authEnabled = remoteWins("auth.enabled") - ? config.auth.enabled - : yield* envOverrideBool( - "SUPABASE_AUTH_ENABLED", - config.auth.enabled, - "auth.enabled", - projectEnvValues, - ); - // `@supabase/config`'s schema only defaults `site_url` when the key is absent, so an - // explicit `site_url = ""` decodes as `""` with no schema-level error, same gap as - // `db.port === 0` above. - const siteUrl = remoteWins("auth.site_url") - ? config.auth.site_url - : ((yield* envOverride("SUPABASE_AUTH_SITE_URL", config.auth.site_url, projectEnvValues)) ?? - config.auth.site_url); - const jwtIssuer = remoteWins("auth.jwt_issuer") - ? config.auth.jwt_issuer - : yield* envOverride("SUPABASE_AUTH_JWT_ISSUER", config.auth.jwt_issuer, projectEnvValues); - const jwtExpiry = remoteWins("auth.jwt_expiry") - ? config.auth.jwt_expiry - : yield* envOverrideUint( - "SUPABASE_AUTH_JWT_EXPIRY", - "auth.jwt_expiry", - config.auth.jwt_expiry, - projectEnvValues, - ); - const additionalRedirectUrlsOverride = remoteWins("auth.additional_redirect_urls") - ? undefined - : yield* envOverride("SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS", undefined, projectEnvValues); - const additionalRedirectUrls = - additionalRedirectUrlsOverride !== undefined - ? additionalRedirectUrlsOverride.split(",") - : config.auth.additional_redirect_urls; - const enableSignup = remoteWins("auth.enable_signup") - ? config.auth.enable_signup - : yield* envOverrideBool( - "SUPABASE_AUTH_ENABLE_SIGNUP", - config.auth.enable_signup, - "auth.enable_signup", - projectEnvValues, - ); - const enableAnonymousSignIns = remoteWins("auth.enable_anonymous_sign_ins") - ? config.auth.enable_anonymous_sign_ins - : yield* envOverrideBool( - "SUPABASE_AUTH_ENABLE_ANONYMOUS_SIGN_INS", - config.auth.enable_anonymous_sign_ins, - "auth.enable_anonymous_sign_ins", - projectEnvValues, - ); - const enableRefreshTokenRotation = remoteWins("auth.enable_refresh_token_rotation") - ? config.auth.enable_refresh_token_rotation - : yield* envOverrideBool( - "SUPABASE_AUTH_ENABLE_REFRESH_TOKEN_ROTATION", - config.auth.enable_refresh_token_rotation, - "auth.enable_refresh_token_rotation", - projectEnvValues, - ); - const refreshTokenReuseInterval = remoteWins("auth.refresh_token_reuse_interval") - ? config.auth.refresh_token_reuse_interval - : yield* envOverrideUint( - "SUPABASE_AUTH_REFRESH_TOKEN_REUSE_INTERVAL", - "auth.refresh_token_reuse_interval", - config.auth.refresh_token_reuse_interval, - projectEnvValues, - ); - const enableManualLinking = remoteWins("auth.enable_manual_linking") - ? config.auth.enable_manual_linking - : yield* envOverrideBool( - "SUPABASE_AUTH_ENABLE_MANUAL_LINKING", - config.auth.enable_manual_linking, - "auth.enable_manual_linking", - projectEnvValues, - ); - const minimumPasswordLength = remoteWins("auth.minimum_password_length") - ? config.auth.minimum_password_length - : yield* envOverrideUint( - "SUPABASE_AUTH_MINIMUM_PASSWORD_LENGTH", - "auth.minimum_password_length", - config.auth.minimum_password_length, - projectEnvValues, - ); - const passwordRequirements = remoteWins("auth.password_requirements") - ? config.auth.password_requirements - : yield* envOverrideAuthPasswordRequirements( - config.auth.password_requirements, - projectEnvValues, - ); + const authEnabled = config.auth.enabled; + const siteUrl = config.auth.site_url; const authDocument = asRecord(document?.["auth"]); - const captchaInput = yield* resolveAuthCaptcha( - authDocument, - config.auth.captcha, - projectEnvValues, - remoteOverrideKeys, - ); - // Reuses {@link resolveConfiguredSigningKeys}, which already gates the file read on - // `authEnabled` internally, rather than duplicating that gate here: a disabled-auth config - // with a configured path must still sign asymmetrically with the default key, not fall back - // to symmetric HS256. + const captchaInput = resolveAuthCaptcha(config.auth.captcha); + // A disabled-auth config with a configured path must still sign asymmetrically with the + // default key, not fall back to symmetric HS256. const signingKey = signingKeysPath !== undefined && signingKeysPath.length > 0 - ? ((yield* resolveConfiguredSigningKeys( - config, - workdir, - projectEnvValues, - remoteOverrideKeys, - )) ?? [DEFAULT_SIGNING_KEY])[0] + ? ((yield* resolveConfiguredSigningKeys(config, workdir)) ?? [DEFAULT_SIGNING_KEY])[0] : undefined; // This block only accumulates the inputs passkey/webauthn/hook/mfa/email/smtp/third_party // validation needs; the checks themselves run once, later, in the single // `validateResolvedConfig` call below (sms/external run separately after it). let authInput: AuthInput | undefined; if (authEnabled) { - // `@supabase/config`'s auth schema has no `passkey`/`webauthn` fields at all, so they're - // read from the raw, post-`env()`-interpolation TOML document instead of the decoded - // `CliConfig`. `authDocument` is `undefined` when a caller hasn't threaded `document` - // through yet, in which case these presence-based checks are simply skipped. const passkeyDoc = asRecord(authDocument?.["passkey"]); const webauthnDoc = asRecord(authDocument?.["webauthn"]); - // Gated on the raw section already being present: only keys already present in the merged - // config are env-bindable, so an absent `[auth.passkey]`/`[auth.webauthn]` section is never - // synthesized from an env override alone. - const passkeyEnabled = remoteWins("auth.passkey.enabled") - ? yield* rawUnmodeledBool(passkeyDoc?.["enabled"], "auth.passkey.enabled") - : passkeyDoc !== undefined - ? yield* envOverrideBool( - "SUPABASE_AUTH_PASSKEY_ENABLED", - yield* rawUnmodeledBool(passkeyDoc["enabled"], "auth.passkey.enabled"), - "auth.passkey.enabled", - projectEnvValues, - ) - : false; - const configuredRpId = - typeof webauthnDoc?.["rp_id"] === "string" ? webauthnDoc["rp_id"] : undefined; - const rpId = remoteWins("auth.webauthn.rp_id") - ? configuredRpId - : webauthnDoc !== undefined - ? yield* envOverride("SUPABASE_AUTH_WEBAUTHN_RP_ID", configuredRpId, projectEnvValues) - : undefined; - const rpOriginsOverride = remoteWins("auth.webauthn.rp_origins") - ? undefined - : webauthnDoc !== undefined - ? yield* envOverride("SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS", undefined, projectEnvValues) - : undefined; - // A raw or `env(...)`-resolved `rp_origins` string is comma-split, not dropped. - const rawRpOrigins = webauthnDoc?.["rp_origins"]; - const rpOrigins = - rpOriginsOverride !== undefined - ? strToArr(rpOriginsOverride) - : Array.isArray(rawRpOrigins) - ? rawRpOrigins - : typeof rawRpOrigins === "string" - ? strToArr(rawRpOrigins) - : undefined; + const passkeyEnabled = + passkeyDoc !== undefined && + (yield* attempt(() => rawUnmodeledBool(passkeyDoc["enabled"], "auth.passkey.enabled"))); + const rpId = asString(webauthnDoc?.["rp_id"]); + const rpOrigins = rawOrigins(webauthnDoc?.["rp_origins"]); const passkey: PasskeyInput | undefined = passkeyEnabled ? { webauthnPresent: webauthnDoc !== undefined, rpId, rpOrigins } : undefined; - // Only enabled hooks are forwarded, in {@link HOOK_TYPE_ORDER}. - const resolvedHooks = yield* resolveAuthHooks( - authDocument, - config.auth.hook, - projectEnvValues, - remoteOverrideKeys, - ); + const resolvedHooks = resolveAuthHooks(config.auth.hook); const hooks: Array = HOOK_TYPE_ORDER.filter( (hookType) => resolvedHooks[HOOK_TYPE_TO_CAMEL[hookType]].enabled, ).map((hookType) => { @@ -2840,11 +850,7 @@ export const resolveLocalConfigValues = Effect.fnUntraced(function* ( return { type: hookType, uri: resolved.uri, secrets: resolved.secrets }; }); - const resolvedMfa = yield* resolveAuthMfa( - config.auth.mfa, - projectEnvValues, - remoteOverrideKeys, - ); + const resolvedMfa = config.auth.mfa; const mfa: ReadonlyArray = [ { label: "totp", @@ -2863,21 +869,9 @@ export const resolveLocalConfigValues = Effect.fnUntraced(function* ( }, ]; - yield* readAuthEmailTemplateContent( - yield* resolveAuthEmail( - config.auth.email, - authDocument, - projectEnvValues, - remoteOverrideKeys, - ), - workdir, - ); + yield* readAuthEmailTemplateContent(resolveAuthEmail(config.auth.email, authDocument), workdir); - const resolvedSmtp = yield* resolveAuthEmailSmtp( - authDocument, - projectEnvValues, - remoteOverrideKeys, - ); + const resolvedSmtp = resolveAuthEmailSmtp(authDocument); const smtp: SmtpInput | undefined = resolvedSmtp === undefined ? undefined @@ -2890,12 +884,6 @@ export const resolveLocalConfigValues = Effect.fnUntraced(function* ( adminEmail: resolvedSmtp.adminEmail, }; - const thirdParty = yield* resolveThirdPartyProviders( - config.auth.third_party, - projectEnvValues, - remoteOverrideKeys, - ); - authInput = { siteUrl: siteUrl ?? "", captcha: captchaInput, @@ -2903,84 +891,32 @@ export const resolveLocalConfigValues = Effect.fnUntraced(function* ( hooks, mfa, smtp, - thirdParty, + thirdParty: yield* attempt(() => resolveThirdPartyProviders(config.auth.third_party)), }; } const functionSlugs = Object.keys(config.functions); - // `edge_runtime.deno_version` is checked unconditionally, with no `edge_runtime.enabled` - // gate, unlike `studio.port`/`local_smtp.port` above. - const denoVersion = remoteWins("edge_runtime.deno_version") - ? config.edge_runtime.deno_version - : yield* envOverrideDenoVersion(config.edge_runtime.deno_version, projectEnvValues); - - // When `analytics.enabled` and `analytics.backend == "bigquery"`, all three GCP fields are - // required. Backend-enum validation is covered at decode time for the config.toml-sourced - // value by `@supabase/config`'s schema, but not for the `SUPABASE_ANALYTICS_BACKEND` - // env-override path — see {@link envOverrideAnalyticsBackend} for that case. - const analyticsEnabled = remoteWins("analytics.enabled") - ? config.analytics.enabled - : yield* envOverrideBool( - "SUPABASE_ANALYTICS_ENABLED", - config.analytics.enabled, - "analytics.enabled", - projectEnvValues, - ); - const analyticsBackend = yield* envOverrideAnalyticsBackend( - config.analytics.backend, - projectEnvValues, - remoteWins("analytics.backend"), + const denoVersion = config.edge_runtime.deno_version; + + const analyticsEnabled = config.analytics.enabled; + const analyticsBackend = yield* attempt(() => + narrowConfigEnum("analytics.backend", config.analytics.backend, ["postgres", "bigquery"]), ); - const gcpProjectId = remoteWins("analytics.gcp_project_id") - ? config.analytics.gcp_project_id - : yield* envOverride( - "SUPABASE_ANALYTICS_GCP_PROJECT_ID", - config.analytics.gcp_project_id, - projectEnvValues, - ); - const gcpProjectNumber = remoteWins("analytics.gcp_project_number") - ? config.analytics.gcp_project_number - : yield* envOverride( - "SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER", - config.analytics.gcp_project_number, - projectEnvValues, - ); - const gcpJwtPath = remoteWins("analytics.gcp_jwt_path") - ? config.analytics.gcp_jwt_path - : yield* envOverride( - "SUPABASE_ANALYTICS_GCP_JWT_PATH", - config.analytics.gcp_jwt_path, - projectEnvValues, - ); + const gcpProjectId = config.analytics.gcp_project_id; + const gcpProjectNumber = config.analytics.gcp_project_number; + const gcpJwtPath = config.analytics.gcp_jwt_path; // The webhooks check isn't "the user disabled a feature": an omitted `enabled` key in a // present `[experimental.webhooks]` section is rejected too — the section exists only so it - // can be turned on, never explicitly off. This hinges on TOML-section presence, which the + // can be turned on, never explicitly off. This hinges on section presence, which the // schema's decode-time default erases (`experimental.webhooks` always decodes to - // `{ enabled: false }` even when absent), so this reads the raw document instead. + // `{ enabled: false }` even when absent), so this reads the document instead. const experimentalDocument = asRecord(document?.["experimental"]); const webhooksPresent = asRecord(experimentalDocument?.["webhooks"]) !== undefined; - // A malformed JSON override needs no separate error path: it flows through unchanged and - // `validateResolvedConfig`'s existing JSON-validity check reports it. - const webhooksEnabled = remoteWins("experimental.webhooks.enabled") - ? config.experimental.webhooks?.enabled === true - : yield* envOverrideBool( - "SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED", - config.experimental.webhooks?.enabled === true, - "experimental.webhooks.enabled", - projectEnvValues, - ); - const pgdeltaFormatOptions = remoteWins("experimental.pgdelta.format_options") - ? (config.experimental.pgdelta?.format_options ?? "") - : ((yield* envOverride( - "SUPABASE_EXPERIMENTAL_PGDELTA_FORMAT_OPTIONS", - config.experimental.pgdelta?.format_options, - projectEnvValues, - )) ?? ""); + const webhooksEnabled = config.experimental.webhooks?.enabled === true; + const pgdeltaFormatOptions = config.experimental.pgdelta?.format_options ?? ""; // Every pure validation check runs in one place, here, rather than interleaved with this - // function's 3 I/O reads (signing keys, api.tls cert/key, email content) at their original - // relative positions — a config broken in two independent ways can report a different one of - // those errors than if checks ran interleaved. + // function's 3 I/O reads (signing keys, api.tls cert/key, email content). const apiInput: ApiInput = { enabled: apiEnabled, port: apiPort, @@ -3019,38 +955,22 @@ export const resolveLocalConfigValues = Effect.fnUntraced(function* ( analytics: analyticsInput, experimental: experimentalInput, }; - yield* Effect.try({ - try: () => validateResolvedConfig(input), - catch: (cause) => (cause instanceof ConfigValidateError ? cause : thrownError(cause)), - }); - // sms/external run after the shared validation call, since both need the env-override-aware - // resolved value rather than the raw decoded config it validates. + yield* attempt(() => validateResolvedConfig(input)); if (authEnabled) { - yield* validateAuthSmsProviders( - authDocument, - config.auth.sms, - projectEnvValues, - remoteOverrideKeys, - ); - yield* validateAuthExternalProviders( - authDocument, - config.auth.external, - projectEnvValues, - remoteOverrideKeys, + yield* attempt(() => validateAuthSmsProviders(resolveAuthSms(config.auth.sms))); + yield* attempt(() => + validateAuthExternalProviders( + resolveAuthExternalProviders(authDocument, config.auth.external), + ), ); } - // Decrypted like `auth.email.smtp.pass`/`auth.captcha.secret`. - const openaiApiKey = remoteWins("studio.openai_api_key") - ? yield* decryptAuthSecret(config.studio.openai_api_key, projectEnvValues) - : yield* decryptAuthSecret( - yield* envOverride( - "SUPABASE_STUDIO_OPENAI_API_KEY", - config.studio.openai_api_key, - projectEnvValues, - ), - projectEnvValues, - ); + const anonKey = yield* attempt(() => + resolveSignedKey(config.auth.anon_key, jwtSecret, signingKey, "anon"), + ); + const serviceRoleKey = yield* attempt(() => + resolveSignedKey(config.auth.service_role_key, jwtSecret, signingKey, "service_role"), + ); return { apiUrl: apiExternalUrl, @@ -3058,18 +978,18 @@ export const resolveLocalConfigValues = Effect.fnUntraced(function* ( dbPort, studioPort, rootKey, - openaiApiKey, + openaiApiKey: config.studio.openai_api_key, authSiteUrl: siteUrl, - authJwtIssuer: jwtIssuer, - authJwtExpiry: jwtExpiry, - authAdditionalRedirectUrls: additionalRedirectUrls, - authEnableSignup: enableSignup, - authEnableAnonymousSignIns: enableAnonymousSignIns, - authEnableRefreshTokenRotation: enableRefreshTokenRotation, - authRefreshTokenReuseInterval: refreshTokenReuseInterval, - authEnableManualLinking: enableManualLinking, - authMinimumPasswordLength: minimumPasswordLength, - authPasswordRequirements: passwordRequirements, + authJwtIssuer: config.auth.jwt_issuer, + authJwtExpiry: config.auth.jwt_expiry, + authAdditionalRedirectUrls: config.auth.additional_redirect_urls, + authEnableSignup: config.auth.enable_signup, + authEnableAnonymousSignIns: config.auth.enable_anonymous_sign_ins, + authEnableRefreshTokenRotation: config.auth.enable_refresh_token_rotation, + authRefreshTokenReuseInterval: config.auth.refresh_token_reuse_interval, + authEnableManualLinking: config.auth.enable_manual_linking, + authMinimumPasswordLength: config.auth.minimum_password_length, + authPasswordRequirements: config.auth.password_requirements, restUrl: apiUrlWithPath(apiExternalUrl, "/rest/v1"), graphqlUrl: apiUrlWithPath(apiExternalUrl, "/graphql/v1"), functionsUrl: apiUrlWithPath(apiExternalUrl, "/functions/v1"), @@ -3077,60 +997,11 @@ export const resolveLocalConfigValues = Effect.fnUntraced(function* ( studioUrl: `http://${hostname}:${studioPort}`, mailpitUrl: `http://${hostname}:${mailpitPort}`, dbUrl: `postgresql://postgres:${DEFAULT_DB_PASSWORD}@${hostname}:${dbPort}/postgres`, - // Decrypted like `anon_key`/`service_role_key` below. - publishableKey: resolveOpaqueKey( - remoteWins("auth.publishable_key") - ? yield* decryptAuthSecret(config.auth.publishable_key, projectEnvValues) - : yield* decryptAuthSecret( - yield* envOverride( - "SUPABASE_AUTH_PUBLISHABLE_KEY", - config.auth.publishable_key, - projectEnvValues, - ), - projectEnvValues, - ), - defaultPublishableKey, - ), - secretKey: resolveOpaqueKey( - remoteWins("auth.secret_key") - ? yield* decryptAuthSecret(config.auth.secret_key, projectEnvValues) - : yield* decryptAuthSecret( - yield* envOverride( - "SUPABASE_AUTH_SECRET_KEY", - config.auth.secret_key, - projectEnvValues, - ), - projectEnvValues, - ), - defaultSecretKey, - ), + publishableKey: resolveOpaqueKey(config.auth.publishable_key, defaultPublishableKey), + secretKey: resolveOpaqueKey(config.auth.secret_key, defaultSecretKey), jwtSecret, - anonKey: yield* resolveSignedKey( - yield* decryptAuthSecret( - remoteWins("auth.anon_key") - ? config.auth.anon_key - : yield* envOverride("SUPABASE_AUTH_ANON_KEY", config.auth.anon_key, projectEnvValues), - projectEnvValues, - ), - jwtSecret, - signingKey, - "anon", - ), - serviceRoleKey: yield* resolveSignedKey( - yield* decryptAuthSecret( - remoteWins("auth.service_role_key") - ? config.auth.service_role_key - : yield* envOverride( - "SUPABASE_AUTH_SERVICE_ROLE_KEY", - config.auth.service_role_key, - projectEnvValues, - ), - projectEnvValues, - ), - jwtSecret, - signingKey, - "service_role", - ), + anonKey, + serviceRoleKey, storageS3Url: apiUrlWithPath(apiExternalUrl, "/storage/v1/s3"), storageS3AccessKeyId: DEFAULT_S3_ACCESS_KEY_ID, storageS3SecretAccessKey: DEFAULT_S3_SECRET_ACCESS_KEY, @@ -3142,174 +1013,44 @@ export const resolveLocalConfigValues = Effect.fnUntraced(function* ( gcpJwtPath: gcpJwtPath ?? "", // Sanitized here, not in `input.projectId` above: `validateResolvedConfig`'s check is // presence-only and must see the raw value to reject an explicit `project_id = ""`. - projectId: sanitizeProjectId(resolvedProjectId ?? ""), + projectId: sanitizeProjectId(resolvedProjectId), edgeRuntimeDenoVersion: denoVersion, } satisfies LocalConfigValues; }); -const asConfigValidateError = (cause: unknown) => - cause instanceof ConfigValidateError - ? cause - : new ConfigValidateError(cause instanceof Error ? cause.message : String(cause)); - /** Resolves local and remote signing keys for services that consume the local JWKS document. */ export const resolveLocalJwks = Effect.fnUntraced(function* ( config: CliConfig, workdir: string, jwtSecret: string, - projectEnvValues?: Readonly>, - remoteOverrideKeys: ReadonlySet = new Set(), ) { - const { issuerUrl, signingKeys, signingKeysPath } = yield* Effect.gen(function* () { - const remoteWins = makeRemoteWins(remoteOverrideKeys); - const signingKeysPath = remoteWins("auth.signing_keys_path") - ? config.auth.signing_keys_path - : yield* envOverride( - "SUPABASE_AUTH_SIGNING_KEYS_PATH", - config.auth.signing_keys_path, - projectEnvValues, - ); - // Every resolved config carries the default ES256 key, regardless of `auth.enabled`. It's - // only ever replaced by a configured `signing_keys_path` file, and only when that file is - // actually read (gated on `auth.enabled` — see {@link resolveConfiguredSigningKeys}). So JWKS - // resolution always publishes either the file's keys or this default, never neither — - // `GOTRUE_JWT_KEYS` signs with the same default, so the two must never disagree. - const signingKeys: ReadonlyArray = (yield* resolveConfiguredSigningKeys( - config, - workdir, - projectEnvValues, - remoteOverrideKeys, - )) ?? [DEFAULT_SIGNING_KEY]; - - // Built as a `ThirdPartyProvidersLike` (every provider's full field set, including auth0's - // `tenant_region`) rather than the validation-only `ThirdPartyInput`, since - // {@link resolveThirdPartyIssuerUrl} needs the full set to build the issuer URL. - const thirdParty: ThirdPartyProvidersLike = { - firebase: { - enabled: remoteWins("auth.third_party.firebase.enabled") - ? config.auth.third_party.firebase.enabled - : yield* envOverrideBool( - "SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED", - config.auth.third_party.firebase.enabled, - "auth.third_party.firebase.enabled", - projectEnvValues, - ), - project_id: remoteWins("auth.third_party.firebase.project_id") - ? config.auth.third_party.firebase.project_id - : yield* envOverride( - "SUPABASE_AUTH_THIRD_PARTY_FIREBASE_PROJECT_ID", - config.auth.third_party.firebase.project_id, - projectEnvValues, - ), - }, - auth0: { - enabled: remoteWins("auth.third_party.auth0.enabled") - ? config.auth.third_party.auth0.enabled - : yield* envOverrideBool( - "SUPABASE_AUTH_THIRD_PARTY_AUTH0_ENABLED", - config.auth.third_party.auth0.enabled, - "auth.third_party.auth0.enabled", - projectEnvValues, - ), - tenant: remoteWins("auth.third_party.auth0.tenant") - ? config.auth.third_party.auth0.tenant - : yield* envOverride( - "SUPABASE_AUTH_THIRD_PARTY_AUTH0_TENANT", - config.auth.third_party.auth0.tenant, - projectEnvValues, - ), - tenant_region: remoteWins("auth.third_party.auth0.tenant_region") - ? config.auth.third_party.auth0.tenant_region - : yield* envOverride( - "SUPABASE_AUTH_THIRD_PARTY_AUTH0_TENANT_REGION", - config.auth.third_party.auth0.tenant_region, - projectEnvValues, - ), - }, - aws_cognito: { - enabled: remoteWins("auth.third_party.aws_cognito.enabled") - ? config.auth.third_party.aws_cognito.enabled - : yield* envOverrideBool( - "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_ENABLED", - config.auth.third_party.aws_cognito.enabled, - "auth.third_party.aws_cognito.enabled", - projectEnvValues, - ), - user_pool_id: remoteWins("auth.third_party.aws_cognito.user_pool_id") - ? config.auth.third_party.aws_cognito.user_pool_id - : yield* envOverride( - "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_USER_POOL_ID", - config.auth.third_party.aws_cognito.user_pool_id, - projectEnvValues, - ), - user_pool_region: remoteWins("auth.third_party.aws_cognito.user_pool_region") - ? config.auth.third_party.aws_cognito.user_pool_region - : yield* envOverride( - "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_USER_POOL_REGION", - config.auth.third_party.aws_cognito.user_pool_region, - projectEnvValues, - ), - }, - clerk: { - enabled: remoteWins("auth.third_party.clerk.enabled") - ? config.auth.third_party.clerk.enabled - : yield* envOverrideBool( - "SUPABASE_AUTH_THIRD_PARTY_CLERK_ENABLED", - config.auth.third_party.clerk.enabled, - "auth.third_party.clerk.enabled", - projectEnvValues, - ), - domain: remoteWins("auth.third_party.clerk.domain") - ? config.auth.third_party.clerk.domain - : yield* envOverride( - "SUPABASE_AUTH_THIRD_PARTY_CLERK_DOMAIN", - config.auth.third_party.clerk.domain, - projectEnvValues, - ), - }, - workos: { - enabled: remoteWins("auth.third_party.workos.enabled") - ? config.auth.third_party.workos.enabled - : yield* envOverrideBool( - "SUPABASE_AUTH_THIRD_PARTY_WORKOS_ENABLED", - config.auth.third_party.workos.enabled, - "auth.third_party.workos.enabled", - projectEnvValues, - ), - issuer_url: remoteWins("auth.third_party.workos.issuer_url") - ? config.auth.third_party.workos.issuer_url - : yield* envOverride( - "SUPABASE_AUTH_THIRD_PARTY_WORKOS_ISSUER_URL", - config.auth.third_party.workos.issuer_url, - projectEnvValues, - ), - }, - }; - - // This function is called unconditionally, regardless of `auth.enabled`, but - // `resolveThirdPartyIssuerUrl`'s "at most one enabled" + required-field checks are only - // meaningful while auth is enabled — `resolveLocalConfigValues` already ran the equivalent - // check in that case. When auth is disabled, that validation is (correctly) skipped, so this - // uses the unchecked, no-throw issuer-url builder instead. - const authEnabled = remoteWins("auth.enabled") - ? config.auth.enabled - : yield* envOverrideBool( - "SUPABASE_AUTH_ENABLED", - config.auth.enabled, - "auth.enabled", - projectEnvValues, - ); - const issuerUrl = Option.fromNullishOr( - yield* Effect.try({ - try: () => - authEnabled - ? resolveThirdPartyIssuerUrl(thirdParty) - : thirdPartyIssuerUrlUnchecked(thirdParty), - catch: asConfigValidateError, - }), - ).pipe(Option.filter((value) => value.length > 0)); - return { issuerUrl, signingKeys, signingKeysPath }; - }).pipe(Effect.mapError(asConfigValidateError)); + const signingKeysPath = config.auth.signing_keys_path; + // Every resolved config carries the default ES256 key, regardless of `auth.enabled`. It's + // only ever replaced by a configured `signing_keys_path` file, and only when that file is + // actually read (gated on `auth.enabled` — see {@link resolveConfiguredSigningKeys}). So JWKS + // resolution always publishes either the file's keys or this default, never neither — + // `GOTRUE_JWT_KEYS` signs with the same default, so the two must never disagree. + const signingKeys: ReadonlyArray = (yield* resolveConfiguredSigningKeys( + config, + workdir, + )) ?? [DEFAULT_SIGNING_KEY]; + + // `resolveThirdPartyIssuerUrl`'s "at most one enabled" + required-field checks are only + // meaningful while auth is enabled; when auth is disabled they are skipped, so the + // unchecked, no-throw issuer-url builder applies instead. + const thirdParty = config.auth.third_party; + const issuerUrl = Option.fromNullishOr( + yield* Effect.try({ + try: () => + config.auth.enabled + ? resolveThirdPartyIssuerUrl(thirdParty) + : thirdPartyIssuerUrlUnchecked(thirdParty), + catch: (cause) => + cause instanceof ConfigValidateError + ? cause + : new ConfigValidateError(cause instanceof Error ? cause.message : String(cause)), + }), + ).pipe(Option.filter((value) => value.length > 0)); const keys: unknown[] = []; if (Option.isSome(issuerUrl)) { keys.push( diff --git a/apps/cli/src/command-internal/local-config-values.unit.test.ts b/apps/cli/src/command-internal/local-config-values.unit.test.ts index 126270f897..79353d4832 100644 --- a/apps/cli/src/command-internal/local-config-values.unit.test.ts +++ b/apps/cli/src/command-internal/local-config-values.unit.test.ts @@ -1,62 +1,47 @@ import { generateKeyPairSync } from "node:crypto"; import { BunServices } from "@effect/platform-bun"; -import { afterEach, describe, expect, it, vi } from "@effect/vitest"; import { CliConfigSchema, type CliConfig } from "@supabase/config"; -import { ConfigProvider, Effect, FileSystem, Layer, Path, Schema } from "effect"; +import { Effect, FileSystem, Path, Schema } from "effect"; import { FetchHttpClient } from "effect/http"; import { importJWK, jwtVerify } from "jose"; +import { afterEach, describe, expect, it, vi } from "@effect/vitest"; -import { useTempWorkdir, withEnvVar } from "../../tests/helpers/command-mocks.ts"; -import { DEFAULT_SIGNING_KEY } from "./go-jwt.ts"; +import { useTempWorkdir } from "../../tests/helpers/command-mocks.ts"; +import { CliConfigValueError } from "../config/cli-config.errors.ts"; +import { DEFAULT_SIGNING_KEY } from "./local-jwt.ts"; import { POSTGRES_DEFAULT_ROOT_KEY, - InvalidAnalyticsBackendEnvOverrideError, - InvalidBoolEnvOverrideError, - InvalidEdgeRuntimePolicyEnvOverrideError, InvalidJwtSecretError, - InvalidPoolModeEnvOverrideError, - InvalidPortEnvOverrideError, - InvalidRealtimeIpVersionEnvOverrideError, - InvalidSessionReplicationRoleEnvOverrideError, - envOverrideApiMaxRows, - envOverrideDefaultPoolSize, - envOverrideEdgeRuntimePolicy, - envOverrideMajorVersion, - envOverrideMaxClientConn, - envOverridePoolMode, - envOverrideRealtimeIpVersion, - envOverrideRealtimeMaxHeaderLength, + narrowConfigEnum, rawUnmodeledBool, resolveAuthCaptcha, resolveAuthEmail, - resolveAuthEmailSmtp, resolveAuthExternalProviders, - resolveAuthExternalUrl, resolveAuthHooks, - resolveAuthMfa, resolveAuthSms, - resolveConfiguredSigningKeys, resolveDbSettingsEnvOverrides, resolveLocalConfigValues, resolveLocalJwks, } from "./local-config-values.ts"; -/** The `SUPABASE_*` overrides a test pins: the only env its `Config` reads see. */ -const testEnv: Record = {}; +const join = (...segments: ReadonlyArray) => + Effect.gen(function* () { + const path = yield* Path.Path; + return path.join(...segments); + }); -const testLayer = Layer.mergeAll( - BunServices.layer, - Layer.succeed( - ConfigProvider.ConfigProvider, - ConfigProvider.fromEnvRecord(testEnv, { preserveEmptyStrings: true }), - ), -); +const mkdirp = (directory: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + yield* fs.makeDirectory(directory, { recursive: true }); + }); -const run = (effect: Effect.Effect) => - Effect.runSync( - withEnvVar("DOTENV_PRIVATE_KEY", undefined, effect).pipe(Effect.provide(testLayer)), - ); +const writeFile = (file: string, contents: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + yield* fs.writeFileString(file, contents); + }); const runLocalJwks = (...args: Parameters) => resolveLocalJwks(...args).pipe( @@ -64,9 +49,6 @@ const runLocalJwks = (...args: Parameters) => Effect.provideService(FetchHttpClient.Fetch, globalThis.fetch), ); -const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); -const decodeJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown)); - const decodeConfig = Schema.decodeUnknownSync(CliConfigSchema); const WORKDIR = "/tmp/local-config-values-test"; @@ -81,4362 +63,979 @@ function generateRsaJwk(): Record { return { ...jwk, alg: "RS256", kid: "test-rsa-kid" }; } -const writeSigningKeys = Effect.fnUntraced(function* ( - workdir: string, - jwks: ReadonlyArray>, -) { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const supabaseDir = path.join(workdir, "supabase"); - yield* fs.makeDirectory(supabaseDir, { recursive: true }); - yield* fs.writeFileString(path.join(supabaseDir, "signing_keys.json"), yield* encodeJson(jwks)); -}); +const decodeJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown)); +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); + +const writeSigningKeys = (workdir: string, jwks: ReadonlyArray>) => + Effect.gen(function* () { + const supabaseDir = yield* join(workdir, "supabase"); + yield* mkdirp(supabaseDir); + yield* writeFile(yield* join(supabaseDir, "signing_keys.json"), yield* encodeJson(jwks)); + }); describe("resolveLocalConfigValues", () => { - it("derives every URL from api.external_url when unset", () => { - const config = baseConfig(); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); + it.live("derives every URL from api.external_url when unset", () => + Effect.gen(function* () { + const config = baseConfig(); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); - expect(values.apiUrl).toBe("http://127.0.0.1:54321"); - expect(values.restUrl).toBe("http://127.0.0.1:54321/rest/v1"); - expect(values.graphqlUrl).toBe("http://127.0.0.1:54321/graphql/v1"); - expect(values.functionsUrl).toBe("http://127.0.0.1:54321/functions/v1"); - expect(values.mcpUrl).toBe("http://127.0.0.1:54321/mcp"); - expect(values.storageS3Url).toBe("http://127.0.0.1:54321/storage/v1/s3"); - expect(values.studioUrl).toBe("http://127.0.0.1:54323"); - expect(values.mailpitUrl).toBe("http://127.0.0.1:54324"); - }); + expect(values.apiUrl).toBe("http://127.0.0.1:54321"); + expect(values.restUrl).toBe("http://127.0.0.1:54321/rest/v1"); + expect(values.graphqlUrl).toBe("http://127.0.0.1:54321/graphql/v1"); + expect(values.functionsUrl).toBe("http://127.0.0.1:54321/functions/v1"); + expect(values.mcpUrl).toBe("http://127.0.0.1:54321/mcp"); + expect(values.storageS3Url).toBe("http://127.0.0.1:54321/storage/v1/s3"); + expect(values.studioUrl).toBe("http://127.0.0.1:54323"); + expect(values.mailpitUrl).toBe("http://127.0.0.1:54324"); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("uses https and the configured port when api.tls.enabled", () => { - const config = baseConfig({ api: { tls: { enabled: true }, port: 54321 } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.apiUrl).toBe("https://127.0.0.1:54321"); - }); + it.live("uses https and the configured port when api.tls.enabled", () => + Effect.gen(function* () { + const config = baseConfig({ api: { tls: { enabled: true }, port: 54321 } }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.apiUrl).toBe("https://127.0.0.1:54321"); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("uses api.external_url verbatim when configured", () => { - const config = baseConfig({ api: { external_url: "https://example.test" } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.apiUrl).toBe("https://example.test"); - expect(values.restUrl).toBe("https://example.test/rest/v1"); - }); + it.live("uses api.external_url verbatim when configured", () => + Effect.gen(function* () { + const config = baseConfig({ api: { external_url: "https://example.test" } }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.apiUrl).toBe("https://example.test"); + expect(values.restUrl).toBe("https://example.test/rest/v1"); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("brackets an IPv6 hostname when building host:port", () => { - const config = baseConfig(); - const values = run(resolveLocalConfigValues(config, "::1", WORKDIR)); - expect(values.apiUrl).toBe("http://[::1]:54321"); - }); + it.live("brackets an IPv6 hostname when building host:port", () => + Effect.gen(function* () { + const config = baseConfig(); + const values = yield* resolveLocalConfigValues(config, "::1", WORKDIR); + expect(values.apiUrl).toBe("http://[::1]:54321"); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("builds the db URL with the hardcoded postgres password", () => { - const config = baseConfig({ db: { port: 54322 } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.dbUrl).toBe("postgresql://postgres:postgres@127.0.0.1:54322/postgres"); - }); + it.live("builds the db URL with the hardcoded postgres password", () => + Effect.gen(function* () { + const config = baseConfig({ db: { port: 54322 } }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.dbUrl).toBe("postgresql://postgres:postgres@127.0.0.1:54322/postgres"); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("falls back to the default JWT secret and opaque keys when unset", () => { - const config = baseConfig(); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.jwtSecret).toBe("super-secret-jwt-token-with-at-least-32-characters-long"); - expect(values.publishableKey).toBe("sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH"); - expect(values.secretKey).toBe("sb_secret_N7UND0UgjKTVK-Uodkm0Hg_xSvEMPvz"); - }); + it.live("falls back to the default JWT secret and opaque keys when unset", () => + Effect.gen(function* () { + const config = baseConfig(); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.jwtSecret).toBe("super-secret-jwt-token-with-at-least-32-characters-long"); + expect(values.publishableKey).toBe("sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH"); + expect(values.secretKey).toBe("sb_secret_N7UND0UgjKTVK-Uodkm0Hg_xSvEMPvz"); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("uses configured opaque keys verbatim when set", () => { - const config = baseConfig({ - auth: { publishable_key: "sb_publishable_custom", secret_key: "sb_secret_custom" }, - }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.publishableKey).toBe("sb_publishable_custom"); - expect(values.secretKey).toBe("sb_secret_custom"); - }); + it.live("uses configured opaque keys verbatim when set", () => + Effect.gen(function* () { + const config = baseConfig({ + auth: { publishable_key: "sb_publishable_custom", secret_key: "sb_secret_custom" }, + }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.publishableKey).toBe("sb_publishable_custom"); + expect(values.secretKey).toBe("sb_secret_custom"); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("signs the default anon/service_role JWTs from the resolved secret", () => { - const config = baseConfig(); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - const [, anonPayload] = values.anonKey.split("."); - const [, serviceRolePayload] = values.serviceRoleKey.split("."); - expect(JSON.parse(Buffer.from(anonPayload ?? "", "base64url").toString())).toMatchObject({ - role: "anon", - }); - expect(JSON.parse(Buffer.from(serviceRolePayload ?? "", "base64url").toString())).toMatchObject( - { role: "service_role" }, - ); - }); + it.live("signs the default anon/service_role JWTs from the resolved secret", () => + Effect.gen(function* () { + const config = baseConfig(); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const [, anonPayload] = values.anonKey.split("."); + const [, serviceRolePayload] = values.serviceRoleKey.split("."); + expect( + yield* decodeJson(Buffer.from(anonPayload ?? "", "base64url").toString()), + ).toMatchObject({ + role: "anon", + }); + expect( + yield* decodeJson(Buffer.from(serviceRolePayload ?? "", "base64url").toString()), + ).toMatchObject({ role: "service_role" }); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("uses configured anon/service_role keys verbatim when set", () => { - const config = baseConfig({ - auth: { anon_key: "configured-anon", service_role_key: "configured-service-role" }, - }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.anonKey).toBe("configured-anon"); - expect(values.serviceRoleKey).toBe("configured-service-role"); - }); + it.live("uses configured anon/service_role keys verbatim when set", () => + Effect.gen(function* () { + const config = baseConfig({ + auth: { anon_key: "configured-anon", service_role_key: "configured-service-role" }, + }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.anonKey).toBe("configured-anon"); + expect(values.serviceRoleKey).toBe("configured-service-role"); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("signs anon/service_role JWTs from a configured jwt_secret", () => { - const config = baseConfig({ auth: { jwt_secret: "a".repeat(32) } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.jwtSecret).toBe("a".repeat(32)); - expect(values.anonKey).not.toBe(""); - }); + it.live("signs anon/service_role JWTs from a configured jwt_secret", () => + Effect.gen(function* () { + const config = baseConfig({ auth: { jwt_secret: "a".repeat(32) } }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.jwtSecret).toBe("a".repeat(32)); + expect(values.anonKey).not.toBe(""); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("rejects a configured jwt_secret shorter than 16 characters", () => { - const config = baseConfig({ auth: { jwt_secret: "a".repeat(15) } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - InvalidJwtSecretError, - ); - }); + it.live("rejects a configured jwt_secret shorter than 16 characters", () => + Effect.gen(function* () { + const config = baseConfig({ auth: { jwt_secret: "a".repeat(15) } }); + expect( + yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)), + ).toBeInstanceOf(InvalidJwtSecretError); + }).pipe(Effect.provide(BunServices.layer)), + ); - describe("encrypted auth secrets", () => { - // This ciphertext decrypts to "value" under the keypair below. - const VAULT_PRIVATE_KEY = "7fd7210cef8f331ee8c55897996aaaafd853a2b20a4dc73d6d75759f65d2a7eb"; - const VAULT_ENCRYPTED = - "encrypted:BKiXH15AyRzeohGyUrmB6cGjSklCrrBjdesQlX1VcXo/Xp20Bi2gGZ3AlIqxPQDmjVAALnhZamKnuY73l8Dz1P+BYiZUgxTSLzdCvdYUyVbNekj2UudbdUizBViERtZkuQwZHIv/"; + it.live("rejects an explicit empty project_id", () => + Effect.gen(function* () { + // An explicit `project_id = ""` overwrites the workdir-basename default with the literal + // empty string, unlike an absent key. + const config = baseConfig({ project_id: "" }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).message, + ).toContain("Missing required field in config: project_id"); + }).pipe(Effect.provide(BunServices.layer)), + ); - const withVaultKey = (effect: Effect.Effect) => - withEnvVar("DOTENV_PRIVATE_KEY", VAULT_PRIVATE_KEY, effect); + it.live("hardcodes the local S3 credentials", () => + Effect.gen(function* () { + const config = baseConfig(); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.storageS3AccessKeyId).toBe("625729a08b95bf1b7ff351a663f3a23c"); + expect(values.storageS3SecretAccessKey).toBe( + "850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907", + ); + expect(values.storageS3Region).toBe("local"); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("decrypts an encrypted: jwt_secret when DOTENV_PRIVATE_KEY is set", () => { - const config = baseConfig({ auth: { jwt_secret: VAULT_ENCRYPTED } }); - expect(() => - run(withVaultKey(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))), - ).toThrow(InvalidJwtSecretError); - }); + describe("non-auth port validation", () => { + // Unlike the malformed/out-of-range cases above, db.port=0 is a required-field failure with + // no `enabled` gate, unlike api.port/studio.port/local_smtp.port. + // api.enabled defaults to true, so this rejection applies without an explicit + // `api.enabled = true`. + it.live("rejects a configured api.port of 0 when api is enabled", () => + Effect.gen(function* () { + const config = baseConfig({ api: { port: 0 } }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).message, + ).toContain("Missing required field in config: api.port"); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("decrypts an encrypted: publishable_key when DOTENV_PRIVATE_KEY is set", () => { - const config = baseConfig({ auth: { publishable_key: VAULT_ENCRYPTED } }); - const values = run(withVaultKey(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))); - expect(values.publishableKey).toBe("value"); - }); + it.live("does not reject a zero api.port when api is disabled", () => + Effect.gen(function* () { + const config = baseConfig({ api: { enabled: false, port: 0 } }); + expect(yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("fails config loading for an encrypted: secret with no private key, matching Go", () => { - const config = baseConfig({ auth: { publishable_key: VAULT_ENCRYPTED } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "failed to parse config: missing private key", - ); - }); + // studio.enabled defaults to true, so this rejection applies without an explicit + // `studio.enabled = true`. + it.live("rejects a configured studio.port of 0 when studio is enabled", () => + Effect.gen(function* () { + const config = baseConfig({ studio: { port: 0 } }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).message, + ).toContain("Missing required field in config: studio.port"); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("decrypts an encrypted: auth.email.smtp.pass, matching Go's Secret-typed Smtp.Pass field", () => { - const document = { auth: { email: { smtp: { enabled: true, pass: VAULT_ENCRYPTED } } } }; - const resolved = run(withVaultKey(resolveAuthEmailSmtp(document.auth, undefined))); - expect(resolved?.pass).toBe("value"); - }); + it.live("does not reject a zero studio.port when studio is disabled", () => + Effect.gen(function* () { + const config = baseConfig({ studio: { enabled: false, port: 0 } }); + expect(yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("decrypts an encrypted: studio.openai_api_key, matching Go's Secret-typed OpenaiApiKey field", () => { - const config = baseConfig({ studio: { openai_api_key: VAULT_ENCRYPTED } }); - const values = run(withVaultKey(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))); - expect(values.openaiApiKey).toBe("value"); - }); + it.live( + "rejects a malformed studio.api_url (unterminated IPv6 literal) when studio is enabled", + () => + Effect.gen(function* () { + const config = baseConfig({ studio: { api_url: "http://[::1" } }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).message, + ).toContain( + `Invalid config for studio.api_url: parse "http://[::1": missing ']' in host`, + ); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("decrypts an encrypted: SUPABASE_AUTH_* env override, not just the config.toml value", () => { - const config = baseConfig(); - const values = run( - withVaultKey( - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, { - SUPABASE_AUTH_SECRET_KEY: VAULT_ENCRYPTED, - }), - ), - ); - expect(values.secretKey).toBe("value"); - }); - }); + it.live("does not reject a malformed studio.api_url when studio is disabled", () => + Effect.gen(function* () { + const config = baseConfig({ studio: { enabled: false, api_url: "http://[::1" } }); + expect(yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("rejects an explicit empty project_id, matching Go's Config.Validate", () => { - // An explicit `project_id = ""` overwrites the workdir-basename default with the literal - // empty string, unlike an absent key. - const config = baseConfig({ project_id: "" }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Missing required field in config: project_id", + it.live("does not throw for the default studio.api_url", () => + Effect.gen(function* () { + const config = baseConfig(); + expect(yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), ); - }); - it("does not reject an absent project_id when the workdir basename sanitizes to a non-empty value", () => { - const config = Schema.decodeSync(CliConfigSchema)({}); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); + // local_smtp.enabled defaults to true, so this rejection applies without an explicit + // `local_smtp.enabled = true`. + it.live("rejects a configured local_smtp.port of 0 when local_smtp is enabled", () => + Effect.gen(function* () { + const config = baseConfig({ local_smtp: { port: 0 } }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).message, + ).toContain("Missing required field in config: local_smtp.port"); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("rejects an absent project_id when the workdir basename sanitizes to empty, matching Go", () => { - // The workdir-basename default still applies with no `project_id` key present, so a workdir - // whose basename sanitizes to empty (e.g. `!!!`) still fails validation. - const config = Schema.decodeSync(CliConfigSchema)({}); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", "/tmp/!!!"))).toThrow( - "Missing required field in config: project_id", + it.live("does not reject a zero local_smtp.port when local_smtp is disabled", () => + Effect.gen(function* () { + const config = baseConfig({ local_smtp: { enabled: false, port: 0 } }); + expect(yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), ); }); - it("lets SUPABASE_PROJECT_ID override an absent project_id whose basename sanitizes to empty", () => { - const config = Schema.decodeSync(CliConfigSchema)({}); - expect(() => - run( - resolveLocalConfigValues(config, "127.0.0.1", "/tmp/!!!", { - SUPABASE_PROJECT_ID: "env-project", - }), - ), - ).not.toThrow(); - }); + describe("db.root_key (unmodeled raw-document field)", () => { + it.live("falls back to the default root key when absent", () => + Effect.gen(function* () { + const config = baseConfig(); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.rootKey).toBe(POSTGRES_DEFAULT_ROOT_KEY); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("lets SUPABASE_PROJECT_ID override an explicit empty project_id", () => { - const config = baseConfig({ project_id: "" }); - expect(() => - run( - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, { - SUPABASE_PROJECT_ID: "env-project", - }), - ), - ).not.toThrow(); - }); + it.live("uses a configured string root_key verbatim", () => + Effect.gen(function* () { + const config = baseConfig(); + const document = { db: { root_key: "custom-root-key" } }; + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document); + expect(values.rootKey).toBe("custom-root-key"); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("hardcodes the Go-parity local S3 credentials", () => { - const config = baseConfig(); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.storageS3AccessKeyId).toBe("625729a08b95bf1b7ff351a663f3a23c"); - expect(values.storageS3SecretAccessKey).toBe( - "850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907", + it.live("rejects a non-string root_key (e.g. a bare TOML integer)", () => + Effect.gen(function* () { + const config = baseConfig(); + const document = { db: { root_key: 12345 } }; + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document))) + .message, + ).toContain("failed to parse config:\ndb.root_key: expected a table"); + }).pipe(Effect.provide(BunServices.layer)), ); - expect(values.storageS3Region).toBe("local"); }); - describe("SUPABASE_AUTH_* env overrides", () => { - const tempRoot = useTempWorkdir("supabase-signing-keys-env-override-test-"); - - const ENV_KEYS = [ - "SUPABASE_AUTH_JWT_SECRET", - "SUPABASE_AUTH_PUBLISHABLE_KEY", - "SUPABASE_AUTH_SECRET_KEY", - "SUPABASE_AUTH_ANON_KEY", - "SUPABASE_AUTH_SERVICE_ROLE_KEY", - "SUPABASE_AUTH_SIGNING_KEYS_PATH", - ] as const; + // storage.buckets validation lives entirely in config-validate.unit.test.ts; this file has no + // bucket-related mechanics of its own. - afterEach(() => { - for (const key of ENV_KEYS) delete testEnv[key]; - }); + describe("experimental.* (experimental.validate())", () => { + // Exercises this function's own fallback when no `document` (5th param) is supplied; the + // required-field/enabled checks themselves live in config-validate.unit.test.ts. + it.live( + "does not throw a present [experimental.webhooks] section without enabled when no document is provided", + () => + Effect.gen(function* () { + const config = baseConfig({ experimental: { webhooks: {} } }); + expect(yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); + }); - it("overrides jwt_secret even when config.toml sets one", () => { - testEnv["SUPABASE_AUTH_JWT_SECRET"] = "b".repeat(32); - const config = baseConfig({ auth: { jwt_secret: "a".repeat(32) } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.jwtSecret).toBe("b".repeat(32)); + describe("resolveAuthCaptcha", () => { + it("returns undefined when captcha is not configured", () => { + expect(resolveAuthCaptcha(undefined)).toBeUndefined(); }); + }); - it("overrides publishable_key/secret_key", () => { - testEnv["SUPABASE_AUTH_PUBLISHABLE_KEY"] = "env-publishable"; - testEnv["SUPABASE_AUTH_SECRET_KEY"] = "env-secret"; + describe("resolveAuthEmail", () => { + it("keeps an explicit empty subject present in the raw document, not omitted", () => { const config = baseConfig({ - auth: { publishable_key: "config-publishable", secret_key: "config-secret" }, + auth: { email: { template: { confirmation: { subject: "", content_path: "x" } } } }, }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.publishableKey).toBe("env-publishable"); - expect(values.secretKey).toBe("env-secret"); + const authDocument = { email: { template: { confirmation: { subject: "" } } } }; + const resolved = resolveAuthEmail(config.auth.email, authDocument); + expect(resolved.template["confirmation"]?.subject).toBe(""); }); - it("overrides anon_key/service_role_key", () => { - testEnv["SUPABASE_AUTH_ANON_KEY"] = "env-anon"; - testEnv["SUPABASE_AUTH_SERVICE_ROLE_KEY"] = "env-service-role"; + it("omits the subject when the key is absent from the raw document", () => { const config = baseConfig({ - auth: { anon_key: "config-anon", service_role_key: "config-service-role" }, + auth: { email: { template: { confirmation: { content_path: "x" } } } }, }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.anonKey).toBe("env-anon"); - expect(values.serviceRoleKey).toBe("env-service-role"); + const authDocument = { email: { template: { confirmation: { content_path: "x" } } } }; + const resolved = resolveAuthEmail(config.auth.email, authDocument); + expect(resolved.template["confirmation"]?.subject).toBeUndefined(); }); + }); - it("treats an empty env var as unset, matching Viper's default", () => { - testEnv["SUPABASE_AUTH_JWT_SECRET"] = ""; - const config = baseConfig({ auth: { jwt_secret: "a".repeat(32) } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.jwtSecret).toBe("a".repeat(32)); - }); + describe("resolveAuthHooks", () => { + const baseHook = { enabled: false, uri: "", secrets: "" }; + const allHooks = { + mfa_verification_attempt: baseHook, + password_verification_attempt: baseHook, + custom_access_token: baseHook, + send_sms: baseHook, + send_email: baseHook, + before_user_created: baseHook, + }; - it("still applies the short-secret validation to an env-provided jwt_secret", () => { - testEnv["SUPABASE_AUTH_JWT_SECRET"] = "too-short"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - InvalidJwtSecretError, - ); + it("leaves every hook disabled when nothing is configured or overridden", () => { + const resolved = resolveAuthHooks(allHooks); + expect(resolved.customAccessToken.enabled).toBe(false); + expect(resolved.mfaVerificationAttempt.enabled).toBe(false); }); - - it.live("overrides signing_keys_path even when config.toml doesn't set one", () => - Effect.gen(function* () { - const jwk = generateRsaJwk(); - yield* writeSigningKeys(tempRoot.current, [jwk]); - testEnv["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "signing_keys.json"; - const config = baseConfig(); - const values = yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - - const publicJwk = { ...jwk, d: undefined, p: undefined, q: undefined, dp: undefined }; - const publicKey = yield* Effect.promise(() => importJWK(publicJwk, "RS256")); - const { protectedHeader } = yield* Effect.promise(() => - jwtVerify(values.anonKey, publicKey), - ); - expect(protectedHeader).toMatchObject({ alg: "RS256", kid: "test-rsa-kid" }); - }).pipe(Effect.provide(testLayer)), - ); - - it.live("prefers an env-provided signing_keys_path over config.toml's", () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const envJwk = { ...generateRsaJwk(), kid: "env-kid" }; - const configJwk = { ...generateRsaJwk(), kid: "config-kid" }; - yield* writeSigningKeys(tempRoot.current, [envJwk]); - const supabaseDir = path.join(tempRoot.current, "supabase"); - yield* fs.writeFileString( - path.join(supabaseDir, "other_keys.json"), - yield* encodeJson([configJwk]), - ); - testEnv["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "signing_keys.json"; - const config = baseConfig({ auth: { signing_keys_path: "other_keys.json" } }); - const values = yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - const [header] = values.anonKey.split("."); - expect(yield* decodeJson(Buffer.from(header ?? "", "base64url").toString())).toMatchObject({ - kid: "env-kid", - }); - }).pipe(Effect.provide(testLayer)), - ); }); - describe("SUPABASE_* env(VAR) indirection (Go's LoadEnvHook)", () => { - // `env(VAR)` indirection resolves inside any string field, including a `SUPABASE_*` override - // value itself, not just a config.toml literal. - const ENV_KEYS = ["SUPABASE_AUTH_JWT_SECRET", "SUPABASE_DB_PORT", "SUPABASE_API_ENABLED"]; - - afterEach(() => { - for (const key of ENV_KEYS) delete testEnv[key]; - delete testEnv["INDIRECT_JWT_SECRET"]; - delete testEnv["INDIRECT_DB_PORT"]; - delete testEnv["INDIRECT_API_ENABLED"]; + describe("resolveAuthExternalProviders", () => { + it("coerces an env(...)-resolved boolean string for an unmodeled/custom provider", () => { + const authDocument = { + external: { + my_custom: { + enabled: "true", + client_id: "custom-client-id", + skip_nonce_check: "false", + email_optional: "TRUE", + }, + }, + }; + const resolved = resolveAuthExternalProviders(authDocument, baseConfig().auth.external); + expect(resolved["my_custom"]?.enabled).toBe(true); + expect(resolved["my_custom"]?.skipNonceCheck).toBe(false); + expect(resolved["my_custom"]?.emailOptional).toBe(true); }); - it("resolves a string override's env(VAR) indirection", () => { - testEnv["SUPABASE_AUTH_JWT_SECRET"] = "env(INDIRECT_JWT_SECRET)"; - testEnv["INDIRECT_JWT_SECRET"] = "c".repeat(32); - const config = baseConfig({ auth: { jwt_secret: "a".repeat(32) } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.jwtSecret).toBe("c".repeat(32)); + it("throws on an unparsable custom-provider boolean string instead of silently disabling it", () => { + const authDocument = { + external: { my_custom: { enabled: "not-a-bool", client_id: "custom-client-id" } }, + }; + expect(() => resolveAuthExternalProviders(authDocument, baseConfig().auth.external)).toThrow( + '"not-a-bool" is not a valid boolean', + ); }); - it("resolves a port override's env(VAR) indirection", () => { - testEnv["SUPABASE_DB_PORT"] = "env(INDIRECT_DB_PORT)"; - testEnv["INDIRECT_DB_PORT"] = "54329"; - const config = baseConfig({ db: { port: 54322 } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.dbUrl).toBe("postgresql://postgres:postgres@127.0.0.1:54329/postgres"); + it("leaves an absent custom-provider boolean field at its schema default without throwing", () => { + const authDocument = { + external: { my_custom: { client_id: "custom-client-id" } }, + }; + const resolved = resolveAuthExternalProviders(authDocument, baseConfig().auth.external); + expect(resolved["my_custom"]?.enabled).toBe(false); }); - it("resolves a bool override's env(VAR) indirection", () => { - testEnv["SUPABASE_API_ENABLED"] = "env(INDIRECT_API_ENABLED)"; - testEnv["INDIRECT_API_ENABLED"] = "false"; - const config = baseConfig({ - api: { enabled: true, tls: { enabled: true, cert_path: "missing-cert.pem" } }, - }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); + it("weakly coerces a raw numeric custom-provider boolean by truthiness", () => { + const authDocument = { + external: { my_custom: { enabled: 1, client_id: "custom-client-id" } }, + }; + const resolved = resolveAuthExternalProviders(authDocument, baseConfig().auth.external); + expect(resolved["my_custom"]?.enabled).toBe(true); }); - it("preserves the env(VAR) literal when the indirected var is unset, matching Go", () => { - testEnv["SUPABASE_AUTH_JWT_SECRET"] = "env(INDIRECT_JWT_SECRET)"; - const config = baseConfig({ auth: { jwt_secret: "a".repeat(32) } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.jwtSecret).toBe("env(INDIRECT_JWT_SECRET)"); + it("throws on a raw array/table custom-provider boolean instead of silently disabling it", () => { + const authDocument = { + external: { my_custom: { enabled: [1, 2], client_id: "custom-client-id" } }, + }; + expect(() => resolveAuthExternalProviders(authDocument, baseConfig().auth.external)).toThrow( + '"1,2" is not a valid boolean', + ); }); }); - describe("non-auth SUPABASE_* env overrides", () => { - const ENV_KEYS = [ - "SUPABASE_DB_PORT", - "SUPABASE_STUDIO_PORT", - "SUPABASE_LOCAL_SMTP_PORT", - "SUPABASE_API_PORT", - "SUPABASE_API_EXTERNAL_URL", - "SUPABASE_STUDIO_API_URL", - ] as const; + describe("rawUnmodeledBool", () => { + it("returns false for an absent value", () => { + expect(rawUnmodeledBool(undefined, "auth.passkey.enabled")).toBe(false); + }); - afterEach(() => { - for (const key of ENV_KEYS) delete testEnv[key]; + it("passes a real boolean through unchanged", () => { + expect(rawUnmodeledBool(true, "auth.passkey.enabled")).toBe(true); + expect(rawUnmodeledBool(false, "auth.passkey.enabled")).toBe(false); }); - it("overrides db.port for the derived DB URL and the exposed dbPort", () => { - testEnv["SUPABASE_DB_PORT"] = "54329"; - const config = baseConfig({ db: { port: 54322 } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.dbUrl).toBe("postgresql://postgres:postgres@127.0.0.1:54329/postgres"); - expect(values.dbPort).toBe(54329); + it("coerces a raw number by truthiness", () => { + expect(rawUnmodeledBool(123, "auth.passkey.enabled")).toBe(true); + expect(rawUnmodeledBool(0, "auth.passkey.enabled")).toBe(false); + expect(rawUnmodeledBool(1.5, "auth.passkey.enabled")).toBe(true); }); - it("overrides studio.port for the derived Studio URL", () => { - testEnv["SUPABASE_STUDIO_PORT"] = "54330"; - const config = baseConfig({ studio: { port: 54323 } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.studioUrl).toBe("http://127.0.0.1:54330"); + it("parses boolean-ish strings", () => { + expect(rawUnmodeledBool("true", "auth.passkey.enabled")).toBe(true); + expect(rawUnmodeledBool("False", "auth.passkey.enabled")).toBe(false); + expect(rawUnmodeledBool("", "auth.passkey.enabled")).toBe(false); }); - it("overrides local_smtp.port for the derived Mailpit URL", () => { - testEnv["SUPABASE_LOCAL_SMTP_PORT"] = "54331"; - const config = baseConfig({ local_smtp: { port: 54324 } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.mailpitUrl).toBe("http://127.0.0.1:54331"); + it("throws on an unparsable string instead of silently disabling it", () => { + expect(() => rawUnmodeledBool("not-a-bool", "auth.passkey.enabled")).toThrow( + '"not-a-bool" is not a valid boolean', + ); }); - it("overrides api.port for every API-derived URL", () => { - testEnv["SUPABASE_API_PORT"] = "54332"; - const config = baseConfig({ api: { port: 54321 } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.apiUrl).toBe("http://127.0.0.1:54332"); - expect(values.restUrl).toBe("http://127.0.0.1:54332/rest/v1"); + it("throws on an array or table value instead of coercing it", () => { + expect(() => rawUnmodeledBool([1, 2], "auth.passkey.enabled")).toThrow(CliConfigValueError); + expect(() => rawUnmodeledBool({ nested: true }, "auth.passkey.enabled")).toThrow( + CliConfigValueError, + ); }); + }); - it("overrides api.external_url even when config.toml sets one", () => { - testEnv["SUPABASE_API_EXTERNAL_URL"] = "https://env-override.example"; - const config = baseConfig({ api: { external_url: "https://config.example" } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.apiUrl).toBe("https://env-override.example"); + describe("resolveDbSettingsEnvOverrides", () => { + it("returns the configured settings unchanged when nothing is overridden", () => { + const settings = { shared_buffers: "128MB", max_connections: 100 }; + expect(resolveDbSettingsEnvOverrides(settings)).toEqual(settings); }); - it("treats an empty non-auth env var as unset, matching Viper's default", () => { - testEnv["SUPABASE_DB_PORT"] = ""; - const config = baseConfig({ db: { port: 54322 } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.dbUrl).toBe("postgresql://postgres:postgres@127.0.0.1:54322/postgres"); + it("leaves an unconfigured field undefined when nothing is overridden", () => { + expect(resolveDbSettingsEnvOverrides({}).effective_cache_size).toBeUndefined(); }); - it.each([ - "SUPABASE_DB_PORT", - "SUPABASE_STUDIO_PORT", - "SUPABASE_LOCAL_SMTP_PORT", - "SUPABASE_API_PORT", - ] as const)("rejects a malformed %s override instead of producing NaN", (envKey) => { - testEnv[envKey] = "abc"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - InvalidPortEnvOverrideError, - ); - }); - - it("rejects a SUPABASE_DB_PORT override above the uint16 range", () => { - testEnv["SUPABASE_DB_PORT"] = "99999"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - InvalidPortEnvOverrideError, - ); - }); - - it("resolves an octal leading-zero SUPABASE_DB_PORT override to its octal value, not decimal", () => { - testEnv["SUPABASE_DB_PORT"] = "010"; - const config = baseConfig({ db: { port: 54322 } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.dbPort).toBe(8); - expect(values.dbUrl).toBe("postgresql://postgres:postgres@127.0.0.1:8/postgres"); - }); - - it("resolves a 0x-prefixed SUPABASE_DB_PORT override as hex", () => { - testEnv["SUPABASE_DB_PORT"] = "0x1F90"; - const config = baseConfig({ db: { port: 54322 } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.dbPort).toBe(8080); - }); - - it("still resolves a plain decimal SUPABASE_DB_PORT override with no leading zero", () => { - testEnv["SUPABASE_DB_PORT"] = "5432"; - const config = baseConfig({ db: { port: 54322 } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.dbPort).toBe(5432); - }); - - it("rejects a 0x-prefixed SUPABASE_DB_PORT override exceeding the uint16 range", () => { - testEnv["SUPABASE_DB_PORT"] = "0x1FFFF"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - InvalidPortEnvOverrideError, - ); - }); - - // Unlike the malformed/out-of-range cases above, db.port=0 is a required-field failure with - // no `enabled` gate, unlike api.port/studio.port/local_smtp.port. - it("rejects a zero SUPABASE_DB_PORT override, matching Go's required-field check", () => { - testEnv["SUPABASE_DB_PORT"] = "0"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Missing required field in config: db.port", - ); - }); - - // api.enabled defaults to true, so this rejection applies without an explicit - // `api.enabled = true`. - it("rejects a configured api.port of 0 when api is enabled", () => { - const config = baseConfig({ api: { port: 0 } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Missing required field in config: api.port", - ); - }); - - it("rejects a zero SUPABASE_API_PORT override when api is enabled", () => { - testEnv["SUPABASE_API_PORT"] = "0"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Missing required field in config: api.port", - ); - }); - - it("does not reject a zero api.port when api is disabled", () => { - const config = baseConfig({ api: { enabled: false, port: 0 } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - - // studio.enabled defaults to true, so this rejection applies without an explicit - // `studio.enabled = true`. - it("rejects a configured studio.port of 0 when studio is enabled", () => { - const config = baseConfig({ studio: { port: 0 } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Missing required field in config: studio.port", - ); - }); - - it("rejects a zero SUPABASE_STUDIO_PORT override when studio is enabled", () => { - testEnv["SUPABASE_STUDIO_PORT"] = "0"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Missing required field in config: studio.port", - ); - }); - - it("does not reject a zero studio.port when studio is disabled", () => { - const config = baseConfig({ studio: { enabled: false, port: 0 } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - - it("rejects a malformed studio.api_url (unterminated IPv6 literal) when studio is enabled", () => { - const config = baseConfig({ studio: { api_url: "http://[::1" } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - `Invalid config for studio.api_url: parse "http://[::1": missing ']' in host`, - ); - }); - - it("does not reject a malformed studio.api_url when studio is disabled", () => { - const config = baseConfig({ studio: { enabled: false, api_url: "http://[::1" } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - - it("does not throw for the default studio.api_url", () => { - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - - it("rejects a malformed SUPABASE_STUDIO_API_URL override", () => { - testEnv["SUPABASE_STUDIO_API_URL"] = "http://[::1"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - `Invalid config for studio.api_url: parse "http://[::1": missing ']' in host`, - ); - }); - - // local_smtp.enabled defaults to true, so this rejection applies without an explicit - // `local_smtp.enabled = true`. - it("rejects a configured local_smtp.port of 0 when local_smtp is enabled", () => { - const config = baseConfig({ local_smtp: { port: 0 } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Missing required field in config: local_smtp.port", - ); - }); - - it("rejects a zero SUPABASE_LOCAL_SMTP_PORT override when local_smtp is enabled", () => { - testEnv["SUPABASE_LOCAL_SMTP_PORT"] = "0"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Missing required field in config: local_smtp.port", - ); - }); - - it("does not reject a zero local_smtp.port when local_smtp is disabled", () => { - const config = baseConfig({ local_smtp: { enabled: false, port: 0 } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - }); - - describe("db.major_version (required field in config)", () => { - // Required-field/range assertions live in config-validate.unit.test.ts; only env-override - // mechanics are tested here. - afterEach(() => { - delete testEnv["SUPABASE_DB_MAJOR_VERSION"]; - }); - - it("overrides a valid configured major_version via SUPABASE_DB_MAJOR_VERSION", () => { - testEnv["SUPABASE_DB_MAJOR_VERSION"] = "15"; - const config = baseConfig({ db: { major_version: 17 } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - - it("rejects an unsupported SUPABASE_DB_MAJOR_VERSION override", () => { - testEnv["SUPABASE_DB_MAJOR_VERSION"] = "16"; - const config = baseConfig({ db: { major_version: 17 } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Failed reading config: Invalid db.major_version: 16.", - ); - }); - - it("rejects a non-numeric SUPABASE_DB_MAJOR_VERSION override", () => { - testEnv["SUPABASE_DB_MAJOR_VERSION"] = "abc"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Failed reading config: Invalid db.major_version: abc.", - ); - }); - - it("treats an empty SUPABASE_DB_MAJOR_VERSION override as unset, matching Viper's default", () => { - testEnv["SUPABASE_DB_MAJOR_VERSION"] = ""; - const config = baseConfig({ db: { major_version: 17 } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - }); - - describe("SUPABASE_DB_NETWORK_RESTRICTIONS_ENABLED env override", () => { - // Always validated eagerly, with no presence/enabled gate — same bucket as - // db.port/db.major_version above. - afterEach(() => { - delete testEnv["SUPABASE_DB_NETWORK_RESTRICTIONS_ENABLED"]; - }); - - it("does not throw for a valid SUPABASE_DB_NETWORK_RESTRICTIONS_ENABLED override", () => { - testEnv["SUPABASE_DB_NETWORK_RESTRICTIONS_ENABLED"] = "true"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - - it("rejects a malformed SUPABASE_DB_NETWORK_RESTRICTIONS_ENABLED override", () => { - testEnv["SUPABASE_DB_NETWORK_RESTRICTIONS_ENABLED"] = "notabool"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - InvalidBoolEnvOverrideError, - ); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - 'Invalid config for db.network_restrictions.enabled: cannot parse "notabool" as a bool', - ); - }); - }); - - describe("db.root_key (unmodeled raw-document field)", () => { - it("falls back to the default root key when absent", () => { - const config = baseConfig(); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.rootKey).toBe(POSTGRES_DEFAULT_ROOT_KEY); - }); - - it("uses a configured string root_key verbatim", () => { - const config = baseConfig(); - const document = { db: { root_key: "custom-root-key" } }; - const values = run( - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), - ); - expect(values.rootKey).toBe("custom-root-key"); - }); - - it("rejects a non-string root_key (e.g. a bare TOML integer), matching Go's Secret decode failure", () => { - const config = baseConfig(); - const document = { db: { root_key: 12345 } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).toThrow( - "failed to parse config: decoding failed due to the following error(s):\n\n'db.root_key' expected a map or struct", - ); - }); - }); - - // storage.buckets validation lives entirely in config-validate.unit.test.ts; this file has no - // bucket-related mechanics of its own. - - // Not gated on edge_runtime.enabled — an invalid value is rejected unconditionally. - describe("edge_runtime.deno_version (required field in config)", () => { - // Required-field/range assertions live in config-validate.unit.test.ts; only env-override - // mechanics are tested here. - afterEach(() => { - delete testEnv["SUPABASE_EDGE_RUNTIME_DENO_VERSION"]; - }); - - it("rejects a zero SUPABASE_EDGE_RUNTIME_DENO_VERSION override", () => { - testEnv["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = "0"; - const config = baseConfig({ edge_runtime: { deno_version: 2 } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Missing required field in config: edge_runtime.deno_version", - ); - }); - - it("rejects an unsupported SUPABASE_EDGE_RUNTIME_DENO_VERSION override", () => { - testEnv["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = "3"; - const config = baseConfig({ edge_runtime: { deno_version: 2 } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Failed reading config: Invalid edge_runtime.deno_version: 3.", - ); - }); - - it("rejects a non-numeric SUPABASE_EDGE_RUNTIME_DENO_VERSION override", () => { - testEnv["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = "abc"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Failed reading config: Invalid edge_runtime.deno_version: abc.", - ); - }); - - it("treats an empty SUPABASE_EDGE_RUNTIME_DENO_VERSION override as unset, matching Viper's default", () => { - testEnv["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = ""; - const config = baseConfig({ edge_runtime: { deno_version: 2 } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - }); - - describe("analytics (BigQuery backend required fields)", () => { - // Required-field/range assertions live in config-validate.unit.test.ts; only env-override - // mechanics are tested here. - afterEach(() => { - delete testEnv["SUPABASE_ANALYTICS_ENABLED"]; - delete testEnv["SUPABASE_ANALYTICS_BACKEND"]; - delete testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_ID"]; - delete testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER"]; - delete testEnv["SUPABASE_ANALYTICS_GCP_JWT_PATH"]; - }); - - it("rejects a bigquery backend enabled only via SUPABASE_ANALYTICS_ENABLED", () => { - testEnv["SUPABASE_ANALYTICS_ENABLED"] = "true"; - const config = baseConfig({ analytics: { enabled: false, backend: "bigquery" } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Missing required field in config: analytics.gcp_project_id", - ); - }); - - it("rejects a bigquery backend selected only via SUPABASE_ANALYTICS_BACKEND", () => { - testEnv["SUPABASE_ANALYTICS_BACKEND"] = "bigquery"; - const config = baseConfig({ analytics: { enabled: true, backend: "postgres" } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Missing required field in config: analytics.gcp_project_id", - ); - }); - - it("accepts env-provided GCP fields overriding empty config.toml values", () => { - testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_ID"] = "proj"; - testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER"] = "123"; - testEnv["SUPABASE_ANALYTICS_GCP_JWT_PATH"] = "gcp.json"; - const config = baseConfig({ analytics: { enabled: true, backend: "bigquery" } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - - it("rejects an invalid SUPABASE_ANALYTICS_BACKEND override", () => { - testEnv["SUPABASE_ANALYTICS_BACKEND"] = "mysql"; - const config = baseConfig({ analytics: { enabled: true, backend: "postgres" } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - InvalidAnalyticsBackendEnvOverrideError, - ); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - 'Invalid config for analytics.backend: cannot parse "mysql" as one of "postgres", "bigquery"', - ); - }); - }); - - describe("experimental.* (experimental.validate())", () => { - // Exercises this function's own fallback when no `document` (5th param) is supplied; the - // required-field/enabled checks themselves live in config-validate.unit.test.ts. - it("does not throw a present [experimental.webhooks] section without enabled when no document is provided", () => { - const config = baseConfig({ experimental: { webhooks: {} } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - - // Required-field/range assertions live in config-validate.unit.test.ts; only env-override - // mechanics are tested here. - afterEach(() => { - delete testEnv["SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED"]; - delete testEnv["SUPABASE_EXPERIMENTAL_PGDELTA_FORMAT_OPTIONS"]; - }); - - it("enables webhooks purely via SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED when the section omits enabled", () => { - testEnv["SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED"] = "true"; - const config = baseConfig({ experimental: { webhooks: {} } }); - const document = { experimental: { webhooks: {} } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).not.toThrow(); - }); - - it("rejects a malformed SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED override on an already-enabled section", () => { - testEnv["SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED"] = "notabool"; - const config = baseConfig({ experimental: { webhooks: { enabled: true } } }); - const document = { experimental: { webhooks: { enabled: true } } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).toThrow(InvalidBoolEnvOverrideError); - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).toThrow( - 'Invalid config for experimental.webhooks.enabled: cannot parse "notabool" as a bool', - ); - }); - - it("rejects an invalid JSON SUPABASE_EXPERIMENTAL_PGDELTA_FORMAT_OPTIONS override", () => { - testEnv["SUPABASE_EXPERIMENTAL_PGDELTA_FORMAT_OPTIONS"] = "{not valid json"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Invalid config for experimental.pgdelta.format_options: must be valid JSON", - ); - }); - - it("accepts a valid JSON SUPABASE_EXPERIMENTAL_PGDELTA_FORMAT_OPTIONS override", () => { - testEnv["SUPABASE_EXPERIMENTAL_PGDELTA_FORMAT_OPTIONS"] = '{"keywordCase":"upper"}'; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - - it("suppresses a malformed SUPABASE_EXPERIMENTAL_PGDELTA_FORMAT_OPTIONS when a remote block already set experimental.pgdelta.format_options (review: PRRT_kwDOErm0O86XLe6o)", () => { - testEnv["SUPABASE_EXPERIMENTAL_PGDELTA_FORMAT_OPTIONS"] = "{not valid json"; - const config = baseConfig({ - experimental: { pgdelta: { format_options: '{"keywordCase":"upper"}' } }, - }); - expect(() => - run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["experimental.pgdelta.format_options"]), - ), - ), - ).not.toThrow(); - }); - }); - - describe("SUPABASE_API_TLS_ENABLED env override", () => { - // Applied before the default `api.external_url` scheme is derived, so it can flip http/https - // even when config.toml disagrees. - afterEach(() => { - delete testEnv["SUPABASE_API_TLS_ENABLED"]; - }); - - it("overrides api.tls.enabled from false to true", () => { - testEnv["SUPABASE_API_TLS_ENABLED"] = "true"; - const config = baseConfig({ api: { tls: { enabled: false }, port: 54321 } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.apiUrl).toBe("https://127.0.0.1:54321"); - }); - - it("overrides api.tls.enabled from true to false", () => { - testEnv["SUPABASE_API_TLS_ENABLED"] = "false"; - const config = baseConfig({ api: { tls: { enabled: true }, port: 54321 } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.apiUrl).toBe("http://127.0.0.1:54321"); - }); - - it("does not override api.tls.enabled once api.external_url is set", () => { - testEnv["SUPABASE_API_TLS_ENABLED"] = "true"; - const config = baseConfig({ api: { external_url: "http://config.example" } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.apiUrl).toBe("http://config.example"); - }); - - it("rejects a malformed override instead of falling back to the configured value", () => { - testEnv["SUPABASE_API_TLS_ENABLED"] = "not-a-bool"; - const config = baseConfig({ api: { tls: { enabled: true }, port: 54321 } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - InvalidBoolEnvOverrideError, - ); - }); - - it("treats an empty override as unset, matching Viper's default", () => { - testEnv["SUPABASE_API_TLS_ENABLED"] = ""; - const config = baseConfig({ api: { tls: { enabled: true }, port: 54321 } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.apiUrl).toBe("https://127.0.0.1:54321"); - }); - }); - - describe("envOverrideRealtimeIpVersion", () => { - afterEach(() => { - delete testEnv["SUPABASE_REALTIME_IP_VERSION"]; - }); - - it("falls back to the configured value when unset", () => { - expect(run(envOverrideRealtimeIpVersion("IPv4", undefined))).toBe("IPv4"); - }); - - it("overrides IPv4 to IPv6 via the env var", () => { - testEnv["SUPABASE_REALTIME_IP_VERSION"] = "IPv6"; - expect(run(envOverrideRealtimeIpVersion("IPv4", undefined))).toBe("IPv6"); - }); - - it("rejects an invalid override instead of falling back to the configured value", () => { - testEnv["SUPABASE_REALTIME_IP_VERSION"] = "IPv5"; - expect(() => run(envOverrideRealtimeIpVersion("IPv4", undefined))).toThrow( - InvalidRealtimeIpVersionEnvOverrideError, - ); - expect(() => run(envOverrideRealtimeIpVersion("IPv4", undefined))).toThrow( - 'Invalid config for realtime.ip_version: cannot parse "IPv5" as one of "IPv4", "IPv6"', - ); - }); - }); - - describe("envOverrideRealtimeMaxHeaderLength", () => { - afterEach(() => { - delete testEnv["SUPABASE_REALTIME_MAX_HEADER_LENGTH"]; - }); - - it("falls back to the configured value when unset", () => { - expect(run(envOverrideRealtimeMaxHeaderLength(4096, undefined))).toBe(4096); - }); - - it("overrides the configured value via the env var", () => { - testEnv["SUPABASE_REALTIME_MAX_HEADER_LENGTH"] = "8192"; - expect(run(envOverrideRealtimeMaxHeaderLength(4096, undefined))).toBe(8192); - }); - - it("also honors a projectEnvValues (dotenv) value", () => { - expect( - run( - envOverrideRealtimeMaxHeaderLength(4096, { - SUPABASE_REALTIME_MAX_HEADER_LENGTH: "16384", - }), - ), - ).toBe(16384); - }); - - it("rejects an override exceeding the uint64 max (2^64), matching Go's ParseUint failure", () => { - testEnv["SUPABASE_REALTIME_MAX_HEADER_LENGTH"] = "18446744073709551616"; - expect(() => run(envOverrideRealtimeMaxHeaderLength(4096, undefined))).toThrow( - "Failed reading config: Invalid realtime.max_header_length: 18446744073709551616.", - ); - }); - - it("accepts an override of exactly the uint64 max (2^64-1)", () => { - testEnv["SUPABASE_REALTIME_MAX_HEADER_LENGTH"] = "18446744073709551615"; - expect(() => run(envOverrideRealtimeMaxHeaderLength(4096, undefined))).not.toThrow(); - }); - - it("rejects a hex override exceeding the uint64 max (2^64), matching Go's ParseUint failure", () => { - testEnv["SUPABASE_REALTIME_MAX_HEADER_LENGTH"] = "0x10000000000000000"; - expect(() => run(envOverrideRealtimeMaxHeaderLength(4096, undefined))).toThrow( - "Failed reading config: Invalid realtime.max_header_length: 0x10000000000000000.", - ); - }); - }); - - describe("envOverrideApiMaxRows", () => { - afterEach(() => { - delete testEnv["SUPABASE_API_MAX_ROWS"]; - }); - - it("falls back to the configured value when unset", () => { - expect(run(envOverrideApiMaxRows(1000, undefined))).toBe(1000); - }); - - it("overrides the configured value via the env var", () => { - testEnv["SUPABASE_API_MAX_ROWS"] = "500"; - expect(run(envOverrideApiMaxRows(1000, undefined))).toBe(500); - }); - }); - - // Exercises `envOverrideMajorVersion`'s base-0 parsing directly; most literals below don't - // correspond to a supported Postgres major version, which the full pipeline would separately - // reject. - describe("envOverrideMajorVersion", () => { - afterEach(() => { - delete testEnv["SUPABASE_DB_MAJOR_VERSION"]; - }); - - it("falls back to the configured value when unset", () => { - expect(run(envOverrideMajorVersion(17, undefined))).toBe(17); - }); - - it("resolves an octal leading-zero override to its octal value, not decimal", () => { - testEnv["SUPABASE_DB_MAJOR_VERSION"] = "010"; - expect(run(envOverrideMajorVersion(17, undefined))).toBe(8); - }); - - it("resolves a 0x-prefixed override as hex", () => { - testEnv["SUPABASE_DB_MAJOR_VERSION"] = "0x10"; - expect(run(envOverrideMajorVersion(17, undefined))).toBe(16); - }); - - it("resolves a 0b-prefixed override as binary", () => { - testEnv["SUPABASE_DB_MAJOR_VERSION"] = "0b101"; - expect(run(envOverrideMajorVersion(17, undefined))).toBe(5); - }); - - it("still resolves a plain decimal override with no leading zero", () => { - testEnv["SUPABASE_DB_MAJOR_VERSION"] = "15"; - expect(run(envOverrideMajorVersion(17, undefined))).toBe(15); - }); - - it("permits an underscore digit separator between decimal digits", () => { - testEnv["SUPABASE_DB_MAJOR_VERSION"] = "1_000"; - expect(run(envOverrideMajorVersion(17, undefined))).toBe(1000); - }); - - it("rejects an invalid octal digit instead of silently falling back to decimal", () => { - testEnv["SUPABASE_DB_MAJOR_VERSION"] = "08"; - expect(() => run(envOverrideMajorVersion(17, undefined))).toThrow( - "Failed reading config: Invalid db.major_version: 08.", - ); - }); - - it("rejects a signed override", () => { - testEnv["SUPABASE_DB_MAJOR_VERSION"] = "+5"; - expect(() => run(envOverrideMajorVersion(17, undefined))).toThrow( - "Failed reading config: Invalid db.major_version: +5.", - ); - }); - }); - - describe("envOverridePoolMode", () => { - afterEach(() => { - delete testEnv["SUPABASE_DB_POOLER_POOL_MODE"]; - }); - - it("falls back to the configured value when unset", () => { - expect(run(envOverridePoolMode("transaction", undefined))).toBe("transaction"); - }); - - it("overrides the configured value via the env var", () => { - testEnv["SUPABASE_DB_POOLER_POOL_MODE"] = "session"; - expect(run(envOverridePoolMode("transaction", undefined))).toBe("session"); - }); - - it("rejects an invalid override instead of falling back to the configured value", () => { - testEnv["SUPABASE_DB_POOLER_POOL_MODE"] = "invalid"; - expect(() => run(envOverridePoolMode("transaction", undefined))).toThrow( - InvalidPoolModeEnvOverrideError, - ); - expect(() => run(envOverridePoolMode("transaction", undefined))).toThrow( - 'Invalid config for db.pooler.pool_mode: cannot parse "invalid" as one of "transaction", "session"', - ); - }); - }); - - describe("envOverrideEdgeRuntimePolicy", () => { - afterEach(() => { - delete testEnv["SUPABASE_EDGE_RUNTIME_POLICY"]; - }); - - it("falls back to the configured value when unset", () => { - expect(run(envOverrideEdgeRuntimePolicy("oneshot", undefined))).toBe("oneshot"); - }); - - it("overrides the configured value via the env var", () => { - testEnv["SUPABASE_EDGE_RUNTIME_POLICY"] = "per_worker"; - expect(run(envOverrideEdgeRuntimePolicy("oneshot", undefined))).toBe("per_worker"); - }); - - it("rejects an invalid override instead of falling back to the configured value", () => { - testEnv["SUPABASE_EDGE_RUNTIME_POLICY"] = "invalid"; - expect(() => run(envOverrideEdgeRuntimePolicy("oneshot", undefined))).toThrow( - InvalidEdgeRuntimePolicyEnvOverrideError, - ); - expect(() => run(envOverrideEdgeRuntimePolicy("oneshot", undefined))).toThrow( - 'Invalid config for edge_runtime.policy: cannot parse "invalid" as one of "per_worker", "oneshot"', - ); - }); - }); - - describe("envOverrideDefaultPoolSize", () => { - afterEach(() => { - delete testEnv["SUPABASE_DB_POOLER_DEFAULT_POOL_SIZE"]; - }); - - it("falls back to the configured value when unset", () => { - expect(run(envOverrideDefaultPoolSize(20, undefined))).toBe(20); - }); - - it("overrides the configured value via the env var", () => { - testEnv["SUPABASE_DB_POOLER_DEFAULT_POOL_SIZE"] = "40"; - expect(run(envOverrideDefaultPoolSize(20, undefined))).toBe(40); - }); - }); - - describe("envOverrideMaxClientConn", () => { - afterEach(() => { - delete testEnv["SUPABASE_DB_POOLER_MAX_CLIENT_CONN"]; - }); - - it("falls back to the configured value when unset", () => { - expect(run(envOverrideMaxClientConn(100, undefined))).toBe(100); - }); - - it("overrides the configured value via the env var", () => { - testEnv["SUPABASE_DB_POOLER_MAX_CLIENT_CONN"] = "200"; - expect(run(envOverrideMaxClientConn(100, undefined))).toBe(200); - }); - }); - - describe("resolveAuthCaptcha", () => { - afterEach(() => { - delete testEnv["SUPABASE_AUTH_CAPTCHA_ENABLED"]; - delete testEnv["SUPABASE_AUTH_CAPTCHA_PROVIDER"]; - delete testEnv["SUPABASE_AUTH_CAPTCHA_SECRET"]; - }); - - it("returns undefined when captcha is not configured", () => { - expect(run(resolveAuthCaptcha(undefined, undefined, undefined))).toBeUndefined(); - }); - - it("overrides enabled/provider when the section is present in the document", () => { - testEnv["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "true"; - testEnv["SUPABASE_AUTH_CAPTCHA_PROVIDER"] = "turnstile"; - const authDocument = { captcha: { enabled: false, provider: "hcaptcha" } }; - const resolved = run( - resolveAuthCaptcha( - authDocument, - { enabled: false, provider: "hcaptcha", secret: "shh" }, - undefined, - ), - ); - expect(resolved?.enabled).toBe(true); - expect(resolved?.provider).toBe("turnstile"); - }); - - it("does not apply an env override when [auth.captcha] is absent from the document", () => { - testEnv["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "true"; - const resolved = run( - resolveAuthCaptcha({}, { enabled: false, provider: "hcaptcha", secret: "shh" }, undefined), - ); - expect(resolved?.enabled).toBe(false); - }); - - it("decrypts an encrypted: captcha secret", () => { - const authDocument = { captcha: { enabled: true } }; - const resolved = run( - withEnvVar( - "DOTENV_PRIVATE_KEY", - "7fd7210cef8f331ee8c55897996aaaafd853a2b20a4dc73d6d75759f65d2a7eb", - resolveAuthCaptcha( - authDocument, - { - enabled: true, - provider: "hcaptcha", - secret: - "encrypted:BKiXH15AyRzeohGyUrmB6cGjSklCrrBjdesQlX1VcXo/Xp20Bi2gGZ3AlIqxPQDmjVAALnhZamKnuY73l8Dz1P+BYiZUgxTSLzdCvdYUyVbNekj2UudbdUizBViERtZkuQwZHIv/", - }, - undefined, - ), - ), - ); - expect(resolved?.secret).toBe("value"); - }); - - it("suppresses a malformed SUPABASE_AUTH_CAPTCHA_ENABLED when a remote block already set auth.captcha.enabled", () => { - testEnv["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "not-a-bool"; - const authDocument = { captcha: { enabled: false } }; - expect(() => - run( - resolveAuthCaptcha( - authDocument, - { enabled: false, provider: "hcaptcha", secret: "shh" }, - undefined, - new Set(["auth.captcha.enabled"]), - ), - ), - ).not.toThrow(); - }); - - it("still rejects a malformed SUPABASE_AUTH_CAPTCHA_ENABLED when no remote block matched", () => { - testEnv["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "not-a-bool"; - const authDocument = { captcha: { enabled: false } }; - expect(() => - run( - resolveAuthCaptcha( - authDocument, - { enabled: false, provider: "hcaptcha", secret: "shh" }, - undefined, - ), - ), - ).toThrow('cannot parse "not-a-bool" as a bool'); - }); - - it("suppresses a malformed SUPABASE_AUTH_CAPTCHA_SECRET when a remote block already set auth.captcha.secret", () => { - testEnv["SUPABASE_AUTH_CAPTCHA_SECRET"] = "encrypted:not-a-real-ciphertext"; - const authDocument = { captcha: { enabled: true } }; - const resolved = run( - resolveAuthCaptcha( - authDocument, - { enabled: true, provider: "hcaptcha", secret: "remote-secret" }, - undefined, - new Set(["auth.captcha.secret"]), - ), - ); - expect(resolved?.secret).toBe("remote-secret"); - }); - - it("still rejects a malformed SUPABASE_AUTH_CAPTCHA_SECRET when no remote block matched", () => { - testEnv["SUPABASE_AUTH_CAPTCHA_SECRET"] = "encrypted:not-a-real-ciphertext"; - const authDocument = { captcha: { enabled: true } }; - expect(() => - run( - resolveAuthCaptcha( - authDocument, - { enabled: true, provider: "hcaptcha", secret: "remote-secret" }, - undefined, - ), - ), - ).toThrow("failed to parse config: missing private key"); - }); - - it("preserves a remote block's valid auth.captcha.provider over an unsupported ambient override", () => { - testEnv["SUPABASE_AUTH_CAPTCHA_PROVIDER"] = "recaptcha"; - const authDocument = { captcha: { enabled: true, provider: "hcaptcha" } }; - const resolved = run( - resolveAuthCaptcha( - authDocument, - { enabled: true, provider: "hcaptcha", secret: "shh" }, - undefined, - new Set(["auth.captcha.provider"]), - ), - ); - expect(resolved?.provider).toBe("hcaptcha"); - }); - - it("still applies SUPABASE_AUTH_CAPTCHA_PROVIDER when no remote block matched", () => { - testEnv["SUPABASE_AUTH_CAPTCHA_PROVIDER"] = "turnstile"; - const authDocument = { captcha: { enabled: true, provider: "hcaptcha" } }; - const resolved = run( - resolveAuthCaptcha( - authDocument, - { enabled: true, provider: "hcaptcha", secret: "shh" }, - undefined, - ), - ); - expect(resolved?.provider).toBe("turnstile"); - }); - }); - - describe("resolveAuthEmail", () => { - afterEach(() => { - delete testEnv["SUPABASE_AUTH_EMAIL_TEMPLATE_CONFIRMATION_SUBJECT"]; - }); - - it("keeps an explicit empty subject present in the raw document, not omitted", () => { - const config = baseConfig({ - auth: { email: { template: { confirmation: { subject: "", content_path: "x" } } } }, - }); - const authDocument = { email: { template: { confirmation: { subject: "" } } } }; - const resolved = run(resolveAuthEmail(config.auth.email, authDocument, undefined)); - expect(resolved.template["confirmation"]?.subject).toBe(""); - }); - - it("omits the subject when the key is absent from the raw document", () => { - const config = baseConfig({ - auth: { email: { template: { confirmation: { content_path: "x" } } } }, - }); - const authDocument = { email: { template: { confirmation: { content_path: "x" } } } }; - const resolved = run(resolveAuthEmail(config.auth.email, authDocument, undefined)); - expect(resolved.template["confirmation"]?.subject).toBeUndefined(); - }); - - it("prefers an env-overridden subject over the raw document's presence, even when absent", () => { - testEnv["SUPABASE_AUTH_EMAIL_TEMPLATE_CONFIRMATION_SUBJECT"] = "Overridden subject"; - const config = baseConfig({ - auth: { email: { template: { confirmation: { content_path: "x" } } } }, - }); - const authDocument = { email: { template: { confirmation: { content_path: "x" } } } }; - const resolved = run(resolveAuthEmail(config.auth.email, authDocument, undefined)); - expect(resolved.template["confirmation"]?.subject).toBe("Overridden subject"); - }); - - describe("max_frequency — remoteOverrideKeys (linked shadow provisioning, CLI-1956)", () => { - afterEach(() => { - delete testEnv["SUPABASE_AUTH_EMAIL_MAX_FREQUENCY"]; - }); - - it("prefers a remote-set auth.email.max_frequency over a conflicting SUPABASE_AUTH_EMAIL_MAX_FREQUENCY", () => { - testEnv["SUPABASE_AUTH_EMAIL_MAX_FREQUENCY"] = "5s"; - const config = baseConfig({ auth: { email: { max_frequency: "1m" } } }); - const resolved = run( - resolveAuthEmail( - config.auth.email, - undefined, - undefined, - new Set(["auth.email.max_frequency"]), - ), - ); - expect(resolved.max_frequency).toBe("1m"); - }); - - it("still applies SUPABASE_AUTH_EMAIL_MAX_FREQUENCY when no remote block matched", () => { - testEnv["SUPABASE_AUTH_EMAIL_MAX_FREQUENCY"] = "5s"; - const config = baseConfig({ auth: { email: { max_frequency: "1m" } } }); - const resolved = run(resolveAuthEmail(config.auth.email, undefined, undefined)); - expect(resolved.max_frequency).toBe("5s"); - }); - }); - }); - - describe("resolveAuthHooks", () => { - const baseHook = { enabled: false, uri: "", secrets: "" }; - const allHooks = { - mfa_verification_attempt: baseHook, - password_verification_attempt: baseHook, - custom_access_token: baseHook, - send_sms: baseHook, - send_email: baseHook, - before_user_created: baseHook, - }; - - afterEach(() => { - delete testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED"]; - delete testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI"]; - delete testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_SECRETS"]; - }); - - it("leaves every hook disabled when nothing is configured or overridden", () => { - const resolved = run(resolveAuthHooks(undefined, allHooks, undefined)); - expect(resolved.customAccessToken.enabled).toBe(false); - expect(resolved.mfaVerificationAttempt.enabled).toBe(false); - }); - - it("overrides enabled/uri when the hook's section is present in the document", () => { - testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED"] = "true"; - testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI"] = "https://example.com/hook"; - const authDocument = { hook: { custom_access_token: { enabled: false } } }; - const resolved = run(resolveAuthHooks(authDocument, allHooks, undefined)); - expect(resolved.customAccessToken.enabled).toBe(true); - expect(resolved.customAccessToken.uri).toBe("https://example.com/hook"); - expect(resolved.mfaVerificationAttempt.enabled).toBe(false); - }); - - it("does not apply an env override when the hook's section is absent from the document", () => { - testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED"] = "true"; - const resolved = run(resolveAuthHooks({}, allHooks, undefined)); - expect(resolved.customAccessToken.enabled).toBe(false); - }); - - it("suppresses a malformed SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED when a remote block already set that hook's enabled", () => { - testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED"] = "not-a-bool"; - const authDocument = { hook: { custom_access_token: { enabled: false } } }; - expect(() => - run( - resolveAuthHooks( - authDocument, - allHooks, - undefined, - new Set(["auth.hook.custom_access_token.enabled"]), - ), - ), - ).not.toThrow(); - }); - - it("still rejects a malformed SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED when no remote block matched", () => { - testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED"] = "not-a-bool"; - const authDocument = { hook: { custom_access_token: { enabled: false } } }; - expect(() => run(resolveAuthHooks(authDocument, allHooks, undefined))).toThrow( - 'cannot parse "not-a-bool" as a bool', - ); - }); - - it("prefers a remote-set auth.hook.custom_access_token.uri over a conflicting SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI", () => { - testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI"] = "ftp://example.com"; - const hooksWithRemoteUri = { - ...allHooks, - custom_access_token: { enabled: true, uri: "https://example.com/hook", secrets: "" }, - }; - const authDocument = { hook: { custom_access_token: { enabled: true } } }; - const resolved = run( - resolveAuthHooks( - authDocument, - hooksWithRemoteUri, - undefined, - new Set(["auth.hook.custom_access_token.uri"]), - ), - ); - expect(resolved.customAccessToken.uri).toBe("https://example.com/hook"); - }); - - it("prefers a remote-set auth.hook.custom_access_token.secrets over a conflicting SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_SECRETS", () => { - testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_SECRETS"] = "env-secret"; - const hooksWithRemoteSecrets = { - ...allHooks, - custom_access_token: { enabled: true, uri: "", secrets: "remote-secret" }, - }; - const authDocument = { hook: { custom_access_token: { enabled: true } } }; - const resolved = run( - resolveAuthHooks( - authDocument, - hooksWithRemoteSecrets, - undefined, - new Set(["auth.hook.custom_access_token.secrets"]), - ), - ); - expect(resolved.customAccessToken.secrets).toBe("remote-secret"); - }); - - it("still applies the env override for uri when no remote block matched that leaf", () => { - testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI"] = "https://env.example.com/hook"; - const hooksWithLocalUri = { - ...allHooks, - custom_access_token: { enabled: true, uri: "https://local.example.com/hook", secrets: "" }, - }; - const authDocument = { hook: { custom_access_token: { enabled: true } } }; - const resolved = run(resolveAuthHooks(authDocument, hooksWithLocalUri, undefined)); - expect(resolved.customAccessToken.uri).toBe("https://env.example.com/hook"); - }); - }); - - describe("resolveAuthMfa — remoteOverrideKeys (linked shadow provisioning, CLI-1956)", () => { - afterEach(() => { - delete testEnv["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"]; - }); - - it("suppresses a malformed SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED when a remote block already set auth.mfa.totp.enroll_enabled", () => { - testEnv["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"] = "not-a-bool"; - const mfa = baseConfig().auth.mfa; - expect(() => - run(resolveAuthMfa(mfa, undefined, new Set(["auth.mfa.totp.enroll_enabled"]))), - ).not.toThrow(); - }); - - it("still rejects a malformed SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED when no remote block matched", () => { - testEnv["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"] = "not-a-bool"; - const mfa = baseConfig().auth.mfa; - expect(() => run(resolveAuthMfa(mfa, undefined))).toThrow( - 'cannot parse "not-a-bool" as a bool', - ); - }); - - it("prefers a remote-set auth.mfa.phone.template over a conflicting SUPABASE_AUTH_MFA_PHONE_TEMPLATE", () => { - testEnv["SUPABASE_AUTH_MFA_PHONE_TEMPLATE"] = "env template"; - const mfa = { - ...baseConfig().auth.mfa, - phone: { ...baseConfig().auth.mfa.phone, template: "remote template" }, - }; - const resolved = run(resolveAuthMfa(mfa, undefined, new Set(["auth.mfa.phone.template"]))); - expect(resolved.phone.template).toBe("remote template"); - delete testEnv["SUPABASE_AUTH_MFA_PHONE_TEMPLATE"]; - }); - - it("still applies SUPABASE_AUTH_MFA_PHONE_TEMPLATE when no remote block matched", () => { - testEnv["SUPABASE_AUTH_MFA_PHONE_TEMPLATE"] = "env template"; - const mfa = { - ...baseConfig().auth.mfa, - phone: { ...baseConfig().auth.mfa.phone, template: "remote template" }, - }; - const resolved = run(resolveAuthMfa(mfa, undefined)); - expect(resolved.phone.template).toBe("env template"); - delete testEnv["SUPABASE_AUTH_MFA_PHONE_TEMPLATE"]; - }); - - it("prefers a remote-set auth.mfa.phone.max_frequency over a conflicting SUPABASE_AUTH_MFA_PHONE_MAX_FREQUENCY", () => { - testEnv["SUPABASE_AUTH_MFA_PHONE_MAX_FREQUENCY"] = "5s"; - const mfa = { - ...baseConfig().auth.mfa, - phone: { ...baseConfig().auth.mfa.phone, max_frequency: "1m" }, - }; - const resolved = run( - resolveAuthMfa(mfa, undefined, new Set(["auth.mfa.phone.max_frequency"])), - ); - expect(resolved.phone.max_frequency).toBe("1m"); - delete testEnv["SUPABASE_AUTH_MFA_PHONE_MAX_FREQUENCY"]; - }); - - it("still applies SUPABASE_AUTH_MFA_PHONE_MAX_FREQUENCY when no remote block matched", () => { - testEnv["SUPABASE_AUTH_MFA_PHONE_MAX_FREQUENCY"] = "5s"; - const mfa = { - ...baseConfig().auth.mfa, - phone: { ...baseConfig().auth.mfa.phone, max_frequency: "1m" }, - }; - const resolved = run(resolveAuthMfa(mfa, undefined)); - expect(resolved.phone.max_frequency).toBe("5s"); - delete testEnv["SUPABASE_AUTH_MFA_PHONE_MAX_FREQUENCY"]; - }); - }); - - describe("resolveAuthEmailSmtp — remoteOverrideKeys (linked shadow provisioning, CLI-1956)", () => { - afterEach(() => { - delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_ENABLED"]; - delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_PASS"]; - }); - - it("suppresses a malformed SUPABASE_AUTH_EMAIL_SMTP_ENABLED when a remote block already set auth.email.smtp.enabled", () => { - testEnv["SUPABASE_AUTH_EMAIL_SMTP_ENABLED"] = "not-a-bool"; - const authDocument = { email: { smtp: { enabled: true } } }; - expect(() => - run(resolveAuthEmailSmtp(authDocument, undefined, new Set(["auth.email.smtp.enabled"]))), - ).not.toThrow(); - }); - - it("still rejects a malformed SUPABASE_AUTH_EMAIL_SMTP_ENABLED when no remote block matched", () => { - testEnv["SUPABASE_AUTH_EMAIL_SMTP_ENABLED"] = "not-a-bool"; - const authDocument = { email: { smtp: { enabled: true } } }; - expect(() => run(resolveAuthEmailSmtp(authDocument, undefined))).toThrow( - 'cannot parse "not-a-bool" as a bool', - ); - }); - - it("suppresses a malformed SUPABASE_AUTH_EMAIL_SMTP_PASS when a remote block already set auth.email.smtp.pass", () => { - testEnv["SUPABASE_AUTH_EMAIL_SMTP_PASS"] = "encrypted:not-a-real-ciphertext"; - const authDocument = { email: { smtp: { enabled: true, pass: "remote-pass" } } }; - const resolved = run( - resolveAuthEmailSmtp(authDocument, undefined, new Set(["auth.email.smtp.pass"])), - ); - expect(resolved?.pass).toBe("remote-pass"); - }); - - it("still rejects a malformed SUPABASE_AUTH_EMAIL_SMTP_PASS when no remote block matched", () => { - testEnv["SUPABASE_AUTH_EMAIL_SMTP_PASS"] = "encrypted:not-a-real-ciphertext"; - const authDocument = { email: { smtp: { enabled: true, pass: "remote-pass" } } }; - expect(() => run(resolveAuthEmailSmtp(authDocument, undefined))).toThrow( - "failed to parse config: missing private key", - ); - }); - - it("prefers a remote-set auth.email.smtp.host over a conflicting SUPABASE_AUTH_EMAIL_SMTP_HOST", () => { - testEnv["SUPABASE_AUTH_EMAIL_SMTP_HOST"] = "smtp.env.example.com"; - const authDocument = { email: { smtp: { enabled: true, host: "smtp.remote.example.com" } } }; - const resolved = run( - resolveAuthEmailSmtp(authDocument, undefined, new Set(["auth.email.smtp.host"])), - ); - expect(resolved?.host).toBe("smtp.remote.example.com"); - delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_HOST"]; - }); - - it("still applies SUPABASE_AUTH_EMAIL_SMTP_HOST when no remote block matched", () => { - testEnv["SUPABASE_AUTH_EMAIL_SMTP_HOST"] = "smtp.env.example.com"; - const authDocument = { email: { smtp: { enabled: true, host: "smtp.remote.example.com" } } }; - const resolved = run(resolveAuthEmailSmtp(authDocument, undefined)); - expect(resolved?.host).toBe("smtp.env.example.com"); - delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_HOST"]; - }); - - it("prefers a remote-set auth.email.smtp.user over a conflicting SUPABASE_AUTH_EMAIL_SMTP_USER", () => { - testEnv["SUPABASE_AUTH_EMAIL_SMTP_USER"] = "env-user"; - const authDocument = { email: { smtp: { enabled: true, user: "remote-user" } } }; - const resolved = run( - resolveAuthEmailSmtp(authDocument, undefined, new Set(["auth.email.smtp.user"])), - ); - expect(resolved?.user).toBe("remote-user"); - delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_USER"]; - }); - - it("still applies SUPABASE_AUTH_EMAIL_SMTP_USER when no remote block matched", () => { - testEnv["SUPABASE_AUTH_EMAIL_SMTP_USER"] = "env-user"; - const authDocument = { email: { smtp: { enabled: true, user: "remote-user" } } }; - const resolved = run(resolveAuthEmailSmtp(authDocument, undefined)); - expect(resolved?.user).toBe("env-user"); - delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_USER"]; - }); - - it("prefers a remote-set auth.email.smtp.admin_email over a conflicting SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL", () => { - testEnv["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"] = "env@example.com"; - const authDocument = { - email: { smtp: { enabled: true, admin_email: "remote@example.com" } }, - }; - const resolved = run( - resolveAuthEmailSmtp(authDocument, undefined, new Set(["auth.email.smtp.admin_email"])), - ); - expect(resolved?.adminEmail).toBe("remote@example.com"); - delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"]; - }); - - it("still applies SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL when no remote block matched", () => { - testEnv["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"] = "env@example.com"; - const authDocument = { - email: { smtp: { enabled: true, admin_email: "remote@example.com" } }, - }; - const resolved = run(resolveAuthEmailSmtp(authDocument, undefined)); - expect(resolved?.adminEmail).toBe("env@example.com"); - delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"]; - }); - - it("prefers a remote-set auth.email.smtp.sender_name over a conflicting SUPABASE_AUTH_EMAIL_SMTP_SENDER_NAME", () => { - testEnv["SUPABASE_AUTH_EMAIL_SMTP_SENDER_NAME"] = "Env Sender"; - const authDocument = { email: { smtp: { enabled: true, sender_name: "Remote Sender" } } }; - const resolved = run( - resolveAuthEmailSmtp(authDocument, undefined, new Set(["auth.email.smtp.sender_name"])), - ); - expect(resolved?.senderName).toBe("Remote Sender"); - delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_SENDER_NAME"]; - }); - - it("still applies SUPABASE_AUTH_EMAIL_SMTP_SENDER_NAME when no remote block matched", () => { - testEnv["SUPABASE_AUTH_EMAIL_SMTP_SENDER_NAME"] = "Env Sender"; - const authDocument = { email: { smtp: { enabled: true, sender_name: "Remote Sender" } } }; - const resolved = run(resolveAuthEmailSmtp(authDocument, undefined)); - expect(resolved?.senderName).toBe("Env Sender"); - delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_SENDER_NAME"]; - }); - }); - - describe("resolveAuthExternalProviders", () => { - it("coerces an env(...)-resolved boolean string for an unmodeled/custom provider", () => { - const authDocument = { - external: { - my_custom: { - enabled: "true", - client_id: "custom-client-id", - skip_nonce_check: "false", - email_optional: "TRUE", - }, - }, - }; - const resolved = run( - resolveAuthExternalProviders(authDocument, baseConfig().auth.external, undefined), - ); - expect(resolved["my_custom"]?.enabled).toBe(true); - expect(resolved["my_custom"]?.skipNonceCheck).toBe(false); - expect(resolved["my_custom"]?.emailOptional).toBe(true); - }); - - it("throws on an unparsable custom-provider boolean string instead of silently disabling it", () => { - const authDocument = { - external: { my_custom: { enabled: "not-a-bool", client_id: "custom-client-id" } }, - }; - expect(() => - run(resolveAuthExternalProviders(authDocument, baseConfig().auth.external, undefined)), - ).toThrow('cannot parse "not-a-bool" as a bool'); - }); - - it("leaves an absent custom-provider boolean field at its schema default without throwing", () => { - const authDocument = { - external: { my_custom: { client_id: "custom-client-id" } }, - }; - const resolved = run( - resolveAuthExternalProviders(authDocument, baseConfig().auth.external, undefined), - ); - expect(resolved["my_custom"]?.enabled).toBe(false); - }); - - it("weakly coerces a raw numeric custom-provider boolean by truthiness, matching Go's WeaklyTypedInput decode", () => { - const authDocument = { - external: { my_custom: { enabled: 1, client_id: "custom-client-id" } }, - }; - const resolved = run( - resolveAuthExternalProviders(authDocument, baseConfig().auth.external, undefined), - ); - expect(resolved["my_custom"]?.enabled).toBe(true); - }); - - it("throws on a raw array/table custom-provider boolean instead of silently disabling it", () => { - const authDocument = { - external: { my_custom: { enabled: [1, 2], client_id: "custom-client-id" } }, - }; - expect(() => - run(resolveAuthExternalProviders(authDocument, baseConfig().auth.external, undefined)), - ).toThrow('cannot parse "1,2" as a bool'); - }); - - it("resolves apple purely from env overrides even with no config.toml [auth.external] section at all, matching Go's ejected default template", () => { - const projectEnvValues = { - SUPABASE_AUTH_EXTERNAL_APPLE_ENABLED: "true", - SUPABASE_AUTH_EXTERNAL_APPLE_CLIENT_ID: "apple-client-id", - SUPABASE_AUTH_EXTERNAL_APPLE_SECRET: "apple-secret", - SUPABASE_AUTH_EXTERNAL_APPLE_URL: "https://appleid.apple.com", - }; - const resolved = run( - resolveAuthExternalProviders(undefined, baseConfig().auth.external, projectEnvValues), - ); - expect(resolved["apple"]).toEqual({ - enabled: true, - clientId: "apple-client-id", - secret: "apple-secret", - url: "https://appleid.apple.com", - redirectUri: "", - skipNonceCheck: false, - emailOptional: false, - }); - }); - - it("does not synthesize any other provider purely from an env override with no TOML table, only apple gets Go's default-template exception", () => { - const projectEnvValues = { - SUPABASE_AUTH_EXTERNAL_GOOGLE_ENABLED: "true", - SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID: "google-client-id", - }; - const resolved = run( - resolveAuthExternalProviders(undefined, baseConfig().auth.external, projectEnvValues), - ); - expect(resolved["google"]).toBeUndefined(); - expect(resolved["apple"]?.enabled).toBe(false); - }); - }); - - describe("resolveAuthExternalProviders — remoteOverrideKeys (linked shadow provisioning, CLI-1956)", () => { - it("prefers a remote-set auth.external..secret over a malformed SUPABASE_AUTH_EXTERNAL__SECRET", () => { - const authDocument = { - external: { my_custom: { enabled: true, secret: "remote-secret" } }, - }; - const projectEnvValues = { SUPABASE_AUTH_EXTERNAL_MY_CUSTOM_SECRET: "encrypted:garbage" }; - const resolved = run( - resolveAuthExternalProviders( - authDocument, - baseConfig().auth.external, - projectEnvValues, - new Set(["auth.external.my_custom.secret"]), - ), - ); - expect(resolved["my_custom"]?.secret).toBe("remote-secret"); - }); - - it("still rejects a malformed SUPABASE_AUTH_EXTERNAL__SECRET when no remote block matched", () => { - const authDocument = { - external: { my_custom: { enabled: true, secret: "remote-secret" } }, - }; - const projectEnvValues = { SUPABASE_AUTH_EXTERNAL_MY_CUSTOM_SECRET: "encrypted:garbage" }; - expect(() => - run( - resolveAuthExternalProviders(authDocument, baseConfig().auth.external, projectEnvValues), - ), - ).toThrow("failed to parse config: missing private key"); - }); - - it("prefers a remote-set auth.external..enabled over a malformed SUPABASE_AUTH_EXTERNAL__ENABLED", () => { - const authDocument = { external: { my_custom: { enabled: true } } }; - const projectEnvValues = { SUPABASE_AUTH_EXTERNAL_MY_CUSTOM_ENABLED: "not-a-bool" }; - expect(() => - run( - resolveAuthExternalProviders( - authDocument, - baseConfig().auth.external, - projectEnvValues, - new Set(["auth.external.my_custom.enabled"]), - ), - ), - ).not.toThrow(); - }); - - it("still rejects a malformed SUPABASE_AUTH_EXTERNAL__ENABLED when no remote block matched", () => { - const authDocument = { external: { my_custom: { enabled: true } } }; - const projectEnvValues = { SUPABASE_AUTH_EXTERNAL_MY_CUSTOM_ENABLED: "not-a-bool" }; - expect(() => - run( - resolveAuthExternalProviders(authDocument, baseConfig().auth.external, projectEnvValues), - ), - ).toThrow('cannot parse "not-a-bool" as a bool'); - }); - - it("prefers a remote-set auth.external..client_id over a conflicting SUPABASE_AUTH_EXTERNAL__CLIENT_ID", () => { - const authDocument = { - external: { my_custom: { enabled: true, client_id: "remote-client-id" } }, - }; - const projectEnvValues = { - SUPABASE_AUTH_EXTERNAL_MY_CUSTOM_CLIENT_ID: "env-should-not-win", - }; - const resolved = run( - resolveAuthExternalProviders( - authDocument, - baseConfig().auth.external, - projectEnvValues, - new Set(["auth.external.my_custom.client_id"]), - ), - ); - expect(resolved["my_custom"]?.clientId).toBe("remote-client-id"); - }); - }); - - describe("rawUnmodeledBool", () => { - it("returns false for an absent value, matching Go's zero-value bool default", () => { - expect(run(rawUnmodeledBool(undefined, "auth.passkey.enabled"))).toBe(false); - }); - - it("passes a real boolean through unchanged", () => { - expect(run(rawUnmodeledBool(true, "auth.passkey.enabled"))).toBe(true); - expect(run(rawUnmodeledBool(false, "auth.passkey.enabled"))).toBe(false); - }); - - it("weakly coerces a raw number by truthiness, matching mapstructure's WeaklyTypedInput decodeBool", () => { - expect(run(rawUnmodeledBool(123, "auth.passkey.enabled"))).toBe(true); - expect(run(rawUnmodeledBool(0, "auth.passkey.enabled"))).toBe(false); - expect(run(rawUnmodeledBool(1.5, "auth.passkey.enabled"))).toBe(true); - }); - - it("parses a valid boolean-ish string the way Go's strconv.ParseBool does", () => { - expect(run(rawUnmodeledBool("true", "auth.passkey.enabled"))).toBe(true); - expect(run(rawUnmodeledBool("False", "auth.passkey.enabled"))).toBe(false); - expect(run(rawUnmodeledBool("", "auth.passkey.enabled"))).toBe(false); - }); - - it("throws on an unparsable string instead of silently disabling it", () => { - expect(() => run(rawUnmodeledBool("not-a-bool", "auth.passkey.enabled"))).toThrow( - 'cannot parse "not-a-bool" as a bool', - ); - }); - - it("throws on an array or table value — mapstructure's decodeBool errors on these unconditionally, never weakly coerced", () => { - expect(() => run(rawUnmodeledBool([1, 2], "auth.passkey.enabled"))).toThrow( - InvalidBoolEnvOverrideError, - ); - expect(() => run(rawUnmodeledBool({ nested: true }, "auth.passkey.enabled"))).toThrow( - InvalidBoolEnvOverrideError, - ); - }); - }); - - describe("resolveDbSettingsEnvOverrides", () => { - const ALL_OVERRIDE_NAMES = [ - "SUPABASE_DB_SETTINGS_EFFECTIVE_CACHE_SIZE", - "SUPABASE_DB_SETTINGS_LOGICAL_DECODING_WORK_MEM", - "SUPABASE_DB_SETTINGS_MAINTENANCE_WORK_MEM", - "SUPABASE_DB_SETTINGS_MAX_CONNECTIONS", - "SUPABASE_DB_SETTINGS_MAX_LOCKS_PER_TRANSACTION", - "SUPABASE_DB_SETTINGS_MAX_PARALLEL_MAINTENANCE_WORKERS", - "SUPABASE_DB_SETTINGS_MAX_PARALLEL_WORKERS", - "SUPABASE_DB_SETTINGS_MAX_PARALLEL_WORKERS_PER_GATHER", - "SUPABASE_DB_SETTINGS_MAX_REPLICATION_SLOTS", - "SUPABASE_DB_SETTINGS_MAX_SLOT_WAL_KEEP_SIZE", - "SUPABASE_DB_SETTINGS_MAX_STANDBY_ARCHIVE_DELAY", - "SUPABASE_DB_SETTINGS_MAX_STANDBY_STREAMING_DELAY", - "SUPABASE_DB_SETTINGS_MAX_WAL_SIZE", - "SUPABASE_DB_SETTINGS_MAX_WAL_SENDERS", - "SUPABASE_DB_SETTINGS_MAX_WORKER_PROCESSES", - "SUPABASE_DB_SETTINGS_SESSION_REPLICATION_ROLE", - "SUPABASE_DB_SETTINGS_SHARED_BUFFERS", - "SUPABASE_DB_SETTINGS_STATEMENT_TIMEOUT", - "SUPABASE_DB_SETTINGS_TRACK_ACTIVITY_QUERY_SIZE", - "SUPABASE_DB_SETTINGS_TRACK_COMMIT_TIMESTAMP", - "SUPABASE_DB_SETTINGS_WAL_KEEP_SIZE", - "SUPABASE_DB_SETTINGS_WAL_SENDER_TIMEOUT", - "SUPABASE_DB_SETTINGS_WORK_MEM", - ]; - - afterEach(() => { - for (const name of ALL_OVERRIDE_NAMES) delete testEnv[name]; - }); - - it("returns the configured settings unchanged when nothing is overridden", () => { - const settings = { shared_buffers: "128MB", max_connections: 100 }; - expect(run(resolveDbSettingsEnvOverrides(settings, undefined))).toEqual(settings); - }); - - it("leaves an unconfigured field undefined when nothing is overridden", () => { - expect( - run(resolveDbSettingsEnvOverrides({}, undefined)).effective_cache_size, - ).toBeUndefined(); - }); - - it("overrides a string field via the env var", () => { - testEnv["SUPABASE_DB_SETTINGS_SHARED_BUFFERS"] = "256MB"; - expect( - run(resolveDbSettingsEnvOverrides({ shared_buffers: "128MB" }, undefined)).shared_buffers, - ).toBe("256MB"); - }); - - it("sets a string field via the env var even when not configured at all", () => { - testEnv["SUPABASE_DB_SETTINGS_WORK_MEM"] = "8MB"; - expect(run(resolveDbSettingsEnvOverrides({}, undefined)).work_mem).toBe("8MB"); - }); - - it("overrides a uint field via the env var", () => { - testEnv["SUPABASE_DB_SETTINGS_MAX_CONNECTIONS"] = "200"; - expect( - run(resolveDbSettingsEnvOverrides({ max_connections: 100 }, undefined)).max_connections, - ).toBe(200); - }); - - it("rejects a non-numeric uint override", () => { - testEnv["SUPABASE_DB_SETTINGS_MAX_CONNECTIONS"] = "not-a-number"; - expect(() => run(resolveDbSettingsEnvOverrides({}, undefined))).toThrow( - "Invalid db.settings.max_connections", - ); - }); - - it("resolves a 0x-prefixed uint override as hex", () => { - testEnv["SUPABASE_DB_SETTINGS_MAX_CONNECTIONS"] = "0x10"; - expect( - run(resolveDbSettingsEnvOverrides({ max_connections: 100 }, undefined)).max_connections, - ).toBe(16); - }); - - it("rejects a uint override exceeding the uint64 max (2^64), matching Go's ParseUint failure", () => { - testEnv["SUPABASE_DB_SETTINGS_MAX_CONNECTIONS"] = "18446744073709551616"; - expect(() => run(resolveDbSettingsEnvOverrides({}, undefined))).toThrow( - "Failed reading config: Invalid db.settings.max_connections: 18446744073709551616.", - ); - }); - - it("overrides the boolean field via the env var", () => { - testEnv["SUPABASE_DB_SETTINGS_TRACK_COMMIT_TIMESTAMP"] = "true"; - expect( - run(resolveDbSettingsEnvOverrides({ track_commit_timestamp: false }, undefined)) - .track_commit_timestamp, - ).toBe(true); - }); - - it("rejects a malformed boolean override", () => { - testEnv["SUPABASE_DB_SETTINGS_TRACK_COMMIT_TIMESTAMP"] = "not-a-bool"; - expect(() => run(resolveDbSettingsEnvOverrides({}, undefined))).toThrow( - InvalidBoolEnvOverrideError, - ); - }); - - it("overrides the session_replication_role enum field via the env var", () => { - testEnv["SUPABASE_DB_SETTINGS_SESSION_REPLICATION_ROLE"] = "replica"; - expect( - run(resolveDbSettingsEnvOverrides({ session_replication_role: "origin" }, undefined)) - .session_replication_role, - ).toBe("replica"); - }); - - it("leaves session_replication_role undefined when neither configured nor overridden", () => { - expect( - run(resolveDbSettingsEnvOverrides({}, undefined)).session_replication_role, - ).toBeUndefined(); - }); - - it("rejects an invalid session_replication_role override", () => { - testEnv["SUPABASE_DB_SETTINGS_SESSION_REPLICATION_ROLE"] = "invalid"; - expect(() => run(resolveDbSettingsEnvOverrides({}, undefined))).toThrow( - InvalidSessionReplicationRoleEnvOverrideError, - ); - expect(() => run(resolveDbSettingsEnvOverrides({}, undefined))).toThrow( - 'Invalid config for db.settings.session_replication_role: cannot parse "invalid" as one of "origin", "replica", "local"', - ); - }); - - it("also honors a projectEnvValues (dotenv) value", () => { - expect( - run(resolveDbSettingsEnvOverrides({}, { SUPABASE_DB_SETTINGS_SHARED_BUFFERS: "512MB" })) - .shared_buffers, - ).toBe("512MB"); - }); - }); - - describe("auth.signing_keys_path (asymmetric JWT signing)", () => { - const tempRoot = useTempWorkdir("supabase-signing-keys-test-"); - - it.live("signs anon/service_role with the first RS256 key in the file", () => - Effect.gen(function* () { - const jwk = generateRsaJwk(); - yield* writeSigningKeys(tempRoot.current, [jwk]); - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - const values = yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - - const publicJwk = { ...jwk, d: undefined, p: undefined, q: undefined, dp: undefined }; - const publicKey = yield* Effect.promise(() => importJWK(publicJwk, "RS256")); - const { payload, protectedHeader } = yield* Effect.promise(() => - jwtVerify(values.anonKey, publicKey), - ); - expect(payload).toMatchObject({ iss: "supabase-demo", role: "anon" }); - expect(protectedHeader).toMatchObject({ alg: "RS256", kid: "test-rsa-kid" }); - - const serviceRole = yield* Effect.promise(() => - jwtVerify(values.serviceRoleKey, publicKey), - ); - expect(serviceRole.payload).toMatchObject({ role: "service_role" }); - }).pipe(Effect.provide(testLayer)), - ); - - it.live("resolves a relative signing_keys_path against /supabase", () => - Effect.gen(function* () { - const jwk = generateRsaJwk(); - yield* writeSigningKeys(tempRoot.current, [jwk]); - const config = baseConfig({ auth: { signing_keys_path: "./signing_keys.json" } }); - const values = yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - expect(values.anonKey.split(".")).toHaveLength(3); - }).pipe(Effect.provide(testLayer)), - ); - - it.live("uses an absolute signing_keys_path as-is, without joining the workdir", () => - Effect.gen(function* () { - const path = yield* Path.Path; - const jwk = generateRsaJwk(); - yield* writeSigningKeys(tempRoot.current, [jwk]); - const absolutePath = path.join(tempRoot.current, "supabase", "signing_keys.json"); - const config = baseConfig({ auth: { signing_keys_path: absolutePath } }); - const values = yield* resolveLocalConfigValues( - config, - "127.0.0.1", - "/some/unrelated/workdir", - ); - expect(values.anonKey.split(".")).toHaveLength(3); - }).pipe(Effect.provide(testLayer)), - ); - - it.live("still prefers an explicit anon_key/service_role_key over signing keys", () => - Effect.gen(function* () { - yield* writeSigningKeys(tempRoot.current, [generateRsaJwk()]); - const config = baseConfig({ - auth: { - signing_keys_path: "signing_keys.json", - anon_key: "configured-anon", - service_role_key: "configured-service-role", - }, - }); - const values = yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - expect(values.anonKey).toBe("configured-anon"); - expect(values.serviceRoleKey).toBe("configured-service-role"); - }).pipe(Effect.provide(testLayer)), - ); - - it.live("falls back to HMAC signing when signing_keys_path resolves to an empty array", () => - Effect.gen(function* () { - yield* writeSigningKeys(tempRoot.current, []); - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - const values = yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - const [, payload] = values.anonKey.split("."); - expect(yield* decodeJson(Buffer.from(payload ?? "", "base64url").toString())).toMatchObject( - { - iss: "supabase-demo", - }, - ); - }).pipe(Effect.provide(testLayer)), - ); - - it.live("throws a Go-worded error when the signing keys file does not exist", () => - Effect.gen(function* () { - const config = baseConfig({ auth: { signing_keys_path: "missing.json" } }); - expect( - (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) - .message, - ).toContain("failed to read signing keys: "); - }).pipe(Effect.provide(testLayer)), - ); - - it.live("throws a Go-worded error when the signing keys file is malformed JSON", () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const supabaseDir = path.join(tempRoot.current, "supabase"); - yield* fs.makeDirectory(supabaseDir, { recursive: true }); - yield* fs.writeFileString(path.join(supabaseDir, "signing_keys.json"), "not valid json"); - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - expect( - (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) - .message, - ).toContain('failed to decode signing keys: JSON Parse error: Unexpected identifier "not"'); - }).pipe(Effect.provide(testLayer)), - ); - - it.live("keeps a leading BOM in the signing keys file, so the file fails to decode", () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const supabaseDir = path.join(tempRoot.current, "supabase"); - yield* fs.makeDirectory(supabaseDir, { recursive: true }); - yield* fs.writeFileString( - path.join(supabaseDir, "signing_keys.json"), - `\uFEFF${yield* encodeJson([generateRsaJwk()])}`, - ); - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - expect( - (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) - .message, - ).toContain("failed to decode signing keys"); - }).pipe(Effect.provide(testLayer)), - ); - - it.live("throws when the first key uses an unsupported algorithm", () => - Effect.gen(function* () { - yield* writeSigningKeys(tempRoot.current, [{ ...generateRsaJwk(), alg: "RS512" }]); - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - expect( - (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) - .message, - ).toContain("unsupported algorithm: RS512"); - }).pipe(Effect.provide(testLayer)), - ); - - it("skips reading a missing signing_keys_path when auth is disabled", () => { - const config = baseConfig({ - auth: { enabled: false, signing_keys_path: "missing.json" }, - }); - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)), - ).not.toThrow(); - }); - - it.live( - "skips reading a malformed signing_keys_path when auth is disabled, but still signs asymmetrically with the default key", - () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const supabaseDir = path.join(tempRoot.current, "supabase"); - yield* fs.makeDirectory(supabaseDir, { recursive: true }); - yield* fs.writeFileString(path.join(supabaseDir, "signing_keys.json"), "not valid json"); - const config = baseConfig({ - auth: { enabled: false, signing_keys_path: "signing_keys.json" }, - }); - const values = yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - // Disabled auth with a configured signing-keys path still signs with the default ES256 key, - // not HMAC — signing depends on whether keys were loaded, not on auth being enabled. - const publicKey = yield* Effect.promise(() => - importJWK({ ...DEFAULT_SIGNING_KEY, d: undefined, key_ops: undefined }, "ES256"), - ); - const { payload, protectedHeader } = yield* Effect.promise(() => - jwtVerify(values.anonKey, publicKey), - ); - expect(payload).toMatchObject({ iss: "supabase-demo", role: "anon" }); - expect(protectedHeader).toMatchObject({ alg: "ES256", kid: DEFAULT_SIGNING_KEY.kid }); - }).pipe(Effect.provide(testLayer)), - ); - - describe("SUPABASE_AUTH_ENABLED env override", () => { - // Reads the post-override `auth.enabled` value, not raw TOML, so an env-only disable/enable - // still gates whether `signing_keys_path` is read. - afterEach(() => { - delete testEnv["SUPABASE_AUTH_ENABLED"]; - }); - - it("skips reading a missing signing_keys_path when auth is disabled only via env", () => { - testEnv["SUPABASE_AUTH_ENABLED"] = "false"; - const config = baseConfig({ - auth: { enabled: true, signing_keys_path: "missing.json" }, - }); - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)), - ).not.toThrow(); - }); - - it.live( - "reads signing_keys_path when auth is enabled only via env despite TOML saying disabled", - () => - Effect.gen(function* () { - testEnv["SUPABASE_AUTH_ENABLED"] = "true"; - const jwk = generateRsaJwk(); - yield* writeSigningKeys(tempRoot.current, [jwk]); - const config = baseConfig({ - auth: { enabled: false, signing_keys_path: "signing_keys.json" }, - }); - const values = yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - expect(values.anonKey.split(".")).toHaveLength(3); - }).pipe(Effect.provide(testLayer)), - ); - - it("rejects a malformed override instead of falling back to the configured value", () => { - testEnv["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; - const config = baseConfig({ - auth: { enabled: false, signing_keys_path: "missing.json" }, - }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))).toThrow( - InvalidBoolEnvOverrideError, - ); - }); - }); - }); - - describe("auth.site_url (required field in config)", () => { - // Required-field/range assertions live in config-validate.unit.test.ts; only env-override - // mechanics are tested here. - describe("SUPABASE_AUTH_ENABLED / SUPABASE_AUTH_SITE_URL env overrides", () => { - afterEach(() => { - delete testEnv["SUPABASE_AUTH_ENABLED"]; - delete testEnv["SUPABASE_AUTH_SITE_URL"]; - }); - - it("rejects an empty site_url when auth is enabled only via env", () => { - testEnv["SUPABASE_AUTH_ENABLED"] = "true"; - const config = baseConfig({ auth: { enabled: false, site_url: "" } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Missing required field in config: auth.site_url", - ); - }); - - it("does not throw when auth is disabled only via env, however empty site_url is", () => { - testEnv["SUPABASE_AUTH_ENABLED"] = "false"; - const config = baseConfig({ auth: { enabled: true, site_url: "" } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - - it("accepts an env-provided site_url overriding an empty config.toml value", () => { - testEnv["SUPABASE_AUTH_SITE_URL"] = "http://localhost:4000"; - const config = baseConfig({ auth: { enabled: true, site_url: "" } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - - it("exposes the overridden site_url on the returned values, not just for validation", () => { - testEnv["SUPABASE_AUTH_SITE_URL"] = "http://localhost:4000"; - const config = baseConfig({ auth: { enabled: true, site_url: "http://127.0.0.1:3000" } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.authSiteUrl).toBe("http://localhost:4000"); - }); - }); - }); - - describe("auth.* flat scalar env overrides (GoTrue container env, not just validation)", () => { - const AUTH_SCALAR_ENV_KEYS = [ - "SUPABASE_AUTH_JWT_ISSUER", - "SUPABASE_AUTH_JWT_EXPIRY", - "SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS", - "SUPABASE_AUTH_ENABLE_SIGNUP", - "SUPABASE_AUTH_ENABLE_ANONYMOUS_SIGN_INS", - "SUPABASE_AUTH_ENABLE_REFRESH_TOKEN_ROTATION", - "SUPABASE_AUTH_REFRESH_TOKEN_REUSE_INTERVAL", - "SUPABASE_AUTH_ENABLE_MANUAL_LINKING", - "SUPABASE_AUTH_MINIMUM_PASSWORD_LENGTH", - "SUPABASE_AUTH_PASSWORD_REQUIREMENTS", - ]; - afterEach(() => { - for (const key of AUTH_SCALAR_ENV_KEYS) delete testEnv[key]; - }); - - it("overrides every flat auth.* scalar GoTrue needs, not just the ones Validate checks", () => { - testEnv["SUPABASE_AUTH_JWT_ISSUER"] = "https://issuer.example.com"; - testEnv["SUPABASE_AUTH_JWT_EXPIRY"] = "7200"; - testEnv["SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS"] = - "https://a.example.com,https://b.example.com"; - testEnv["SUPABASE_AUTH_ENABLE_SIGNUP"] = "false"; - testEnv["SUPABASE_AUTH_ENABLE_ANONYMOUS_SIGN_INS"] = "true"; - testEnv["SUPABASE_AUTH_ENABLE_REFRESH_TOKEN_ROTATION"] = "false"; - testEnv["SUPABASE_AUTH_REFRESH_TOKEN_REUSE_INTERVAL"] = "20"; - testEnv["SUPABASE_AUTH_ENABLE_MANUAL_LINKING"] = "true"; - testEnv["SUPABASE_AUTH_MINIMUM_PASSWORD_LENGTH"] = "12"; - testEnv["SUPABASE_AUTH_PASSWORD_REQUIREMENTS"] = "lower_upper_letters_digits"; - - const config = baseConfig({ - auth: { - jwt_expiry: 3600, - additional_redirect_urls: [], - enable_signup: true, - enable_anonymous_sign_ins: false, - enable_refresh_token_rotation: true, - refresh_token_reuse_interval: 10, - enable_manual_linking: false, - minimum_password_length: 6, - password_requirements: "", - }, - }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - - expect(values.authJwtIssuer).toBe("https://issuer.example.com"); - expect(values.authJwtExpiry).toBe(7200); - expect(values.authAdditionalRedirectUrls).toEqual([ - "https://a.example.com", - "https://b.example.com", - ]); - expect(values.authEnableSignup).toBe(false); - expect(values.authEnableAnonymousSignIns).toBe(true); - expect(values.authEnableRefreshTokenRotation).toBe(false); - expect(values.authRefreshTokenReuseInterval).toBe(20); - expect(values.authEnableManualLinking).toBe(true); - expect(values.authMinimumPasswordLength).toBe(12); - expect(values.authPasswordRequirements).toBe("lower_upper_letters_digits"); - }); - - it("rejects an unrecognized SUPABASE_AUTH_PASSWORD_REQUIREMENTS override, matching Go's UnmarshalText", () => { - testEnv["SUPABASE_AUTH_PASSWORD_REQUIREMENTS"] = "bogus"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Invalid auth.password_requirements: bogus", - ); - }); - }); - - // Required-field/range assertions live in config-validate.unit.test.ts; only env-override - // mechanics are tested here. - - describe("auth.captcha env overrides", () => { - afterEach(() => { - delete testEnv["SUPABASE_AUTH_CAPTCHA_ENABLED"]; - delete testEnv["SUPABASE_AUTH_CAPTCHA_PROVIDER"]; - delete testEnv["SUPABASE_AUTH_CAPTCHA_SECRET"]; - }); - - it("rejects a captcha section enabled only via env with no provider", () => { - testEnv["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "true"; - const config = baseConfig({ auth: { captcha: { enabled: false } } }); - const document = { auth: { captcha: { enabled: false } } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).toThrow("Missing required field in config: auth.captcha.provider"); - }); - - it("does not throw when an incomplete enabled captcha section is disabled only via env", () => { - testEnv["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "false"; - const config = baseConfig({ auth: { captcha: { enabled: true } } }); - const document = { auth: { captcha: { enabled: true } } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).not.toThrow(); - }); - - it("accepts env-provided provider/secret overriding an enabled captcha section", () => { - testEnv["SUPABASE_AUTH_CAPTCHA_PROVIDER"] = "hcaptcha"; - testEnv["SUPABASE_AUTH_CAPTCHA_SECRET"] = "shh"; - const config = baseConfig({ auth: { captcha: { enabled: true } } }); - const document = { auth: { captcha: { enabled: true } } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).not.toThrow(); - }); - - it("does not synthesize a captcha section purely from an env override when [auth.captcha] is absent", () => { - testEnv["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "true"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - }); - - describe("auth.passkey / auth.webauthn env overrides", () => { - // `auth.passkey`/`auth.webauthn` have no decoded-schema presence signal, so these tests thread - // a raw `document` object through explicitly instead of relying on `baseConfig`. - afterEach(() => { - delete testEnv["SUPABASE_AUTH_PASSKEY_ENABLED"]; - delete testEnv["SUPABASE_AUTH_WEBAUTHN_RP_ID"]; - delete testEnv["SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS"]; - }); - - it("rejects a passkey section enabled only via env with no [auth.webauthn] section", () => { - testEnv["SUPABASE_AUTH_PASSKEY_ENABLED"] = "true"; - const config = baseConfig(); - const document = { auth: { passkey: { enabled: false } } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).toThrow( - "Missing required config section: auth.webauthn (required when auth.passkey.enabled is true)", - ); - }); - - it("accepts env-provided rp_id/rp_origins overriding an incomplete [auth.webauthn] section", () => { - testEnv["SUPABASE_AUTH_PASSKEY_ENABLED"] = "true"; - testEnv["SUPABASE_AUTH_WEBAUTHN_RP_ID"] = "localhost"; - testEnv["SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS"] = "http://localhost:3000,http://localhost:3001"; - const config = baseConfig(); - const document = { auth: { passkey: { enabled: false }, webauthn: {} } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).not.toThrow(); - }); - - it("does not synthesize a passkey section purely from an env override when [auth.passkey] is absent from the document", () => { - testEnv["SUPABASE_AUTH_PASSKEY_ENABLED"] = "true"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - - it("throws on an unparsable raw auth.passkey.enabled string instead of silently disabling it", () => { - const config = baseConfig(); - const document = { auth: { passkey: { enabled: "not-a-bool" } } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).toThrow('cannot parse "not-a-bool" as a bool'); - }); - }); - - describe("auth.hook.* env overrides", () => { - // The hook schema always decodes a default `{ enabled: false }` regardless of file presence, - // so presence here is read from the raw `document`, not the decoded `config`. - afterEach(() => { - delete testEnv["SUPABASE_AUTH_HOOK_SEND_EMAIL_ENABLED"]; - delete testEnv["SUPABASE_AUTH_HOOK_SEND_EMAIL_URI"]; - delete testEnv["SUPABASE_AUTH_HOOK_SEND_EMAIL_SECRETS"]; - }); - - it("rejects a hook section enabled only via env with no uri", () => { - testEnv["SUPABASE_AUTH_HOOK_SEND_EMAIL_ENABLED"] = "true"; - const config = baseConfig(); - const document = { auth: { hook: { send_email: { enabled: false } } } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).toThrow("Missing required field in config: auth.hook.send_email.uri"); - }); - - it("accepts an env-provided uri overriding a TOML-enabled hook missing its uri", () => { - testEnv["SUPABASE_AUTH_HOOK_SEND_EMAIL_URI"] = "pg-functions://postgres/auth/hook"; - const config = baseConfig({ auth: { hook: { send_email: { enabled: true } } } }); - const document = { auth: { hook: { send_email: { enabled: true } } } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).not.toThrow(); - }); - - it("does not synthesize a hook enablement purely from an env override when the section is absent from the document", () => { - testEnv["SUPABASE_AUTH_HOOK_SEND_EMAIL_ENABLED"] = "true"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - }); - - describe("auth.email.smtp env overrides", () => { - afterEach(() => { - delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_ENABLED"]; - delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_HOST"]; - delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_PORT"]; - delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_USER"]; - delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_PASS"]; - delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"]; - }); - - it("rejects an smtp section enabled only via env with no host", () => { - testEnv["SUPABASE_AUTH_EMAIL_SMTP_ENABLED"] = "true"; - const config = baseConfig(); - const document = { auth: { email: { smtp: { enabled: false } } } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).toThrow("Missing required field in config: auth.email.smtp.host"); - }); - - it("accepts env-provided host/port/user/pass/admin_email overriding an enabled-but-incomplete smtp section", () => { - testEnv["SUPABASE_AUTH_EMAIL_SMTP_HOST"] = "smtp.example.com"; - testEnv["SUPABASE_AUTH_EMAIL_SMTP_PORT"] = "587"; - testEnv["SUPABASE_AUTH_EMAIL_SMTP_USER"] = "user"; - testEnv["SUPABASE_AUTH_EMAIL_SMTP_PASS"] = "pass"; - testEnv["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"] = "admin@example.com"; - const config = baseConfig(); - const document = { auth: { email: { smtp: { enabled: true } } } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).not.toThrow(); - }); - - it("rejects an invalid SUPABASE_AUTH_EMAIL_SMTP_PORT override", () => { - testEnv["SUPABASE_AUTH_EMAIL_SMTP_HOST"] = "smtp.example.com"; - testEnv["SUPABASE_AUTH_EMAIL_SMTP_PORT"] = "not-a-port"; - testEnv["SUPABASE_AUTH_EMAIL_SMTP_USER"] = "user"; - testEnv["SUPABASE_AUTH_EMAIL_SMTP_PASS"] = "pass"; - testEnv["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"] = "admin@example.com"; - const config = baseConfig(); - const document = { auth: { email: { smtp: { enabled: true } } } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).toThrow(InvalidPortEnvOverrideError); - }); - - it("does not synthesize an smtp section purely from an env override when [auth.email.smtp] is absent from the document", () => { - testEnv["SUPABASE_AUTH_EMAIL_SMTP_ENABLED"] = "true"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - }); - - describe("auth.mfa env overrides", () => { - // These are plain value-typed fields with no presence gate, unlike hooks/smtp above — they're - // overridable unconditionally. - afterEach(() => { - delete testEnv["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"]; - delete testEnv["SUPABASE_AUTH_MFA_TOTP_VERIFY_ENABLED"]; - }); - - it("rejects an env-enabled enroll factor left at its TOML-decoded verify default", () => { - testEnv["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"] = "true"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Invalid MFA config: auth.mfa.totp.enroll_enabled requires verify_enabled", - ); - }); - - it("accepts an env-enabled enroll factor when verify is also env-enabled", () => { - testEnv["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"] = "true"; - testEnv["SUPABASE_AUTH_MFA_TOTP_VERIFY_ENABLED"] = "true"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - - it("rejects a malformed SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED override", () => { - testEnv["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"] = "not-a-bool"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - InvalidBoolEnvOverrideError, - ); - }); - }); - - describe("auth.third_party env overrides", () => { - // Same as auth.mfa above — including workos, whose default template omits the whole section - // yet leaves it still unconditionally overridable. - afterEach(() => { - delete testEnv["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED"]; - delete testEnv["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_PROJECT_ID"]; - }); - - it("rejects a third-party provider enabled only via env with no required field configured", () => { - testEnv["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED"] = "true"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Invalid config: auth.third_party.firebase is enabled but without a project_id.", - ); - }); - - it("accepts an env-provided project_id overriding a TOML-enabled firebase provider", () => { - testEnv["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_PROJECT_ID"] = "my-project"; - const config = baseConfig({ auth: { third_party: { firebase: { enabled: true } } } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - - it("does not enable a third-party provider purely from a required-field env override", () => { - testEnv["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_PROJECT_ID"] = "my-project"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - }); - - describe("auth.email.template/notification (content_path validation)", () => { - const tempRoot = useTempWorkdir("supabase-email-templates-test-"); - - it.live("rejects a template content_path pointing at a missing file", () => - Effect.gen(function* () { - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { template: { invite: { content_path: "missing-invite.html" } } }, - }, - }); - expect( - (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) - .message, - ).toContain("Invalid config for auth.email.template.invite.content_path: "); - }).pipe(Effect.provide(testLayer)), - ); - - it.live("rejects an absolute template content_path outside the project root", () => - Effect.gen(function* () { - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { template: { invite: { content_path: "/etc/hosts" } } }, - }, - }); - expect( - (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) - .message, - ).toContain( - 'Invalid config for auth.email.template.invite.content_path: "/etc/hosts" resolves outside the project root', - ); - }).pipe(Effect.provide(testLayer)), - ); - - it.live( - "resolves a relative template content_path against the workdir itself, not /supabase", - () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - yield* fs.writeFileString(path.join(tempRoot.current, "invite.html"), ""); - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { template: { invite: { content_path: "invite.html" } } }, - }, - }); - yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - }).pipe(Effect.provide(testLayer)), - ); - - it("does not throw a template with no content_path configured", () => { - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { template: { invite: {} } }, - }, - }); - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)), - ).not.toThrow(); - }); - - it.live("rejects an enabled notification content_path pointing at a missing file", () => - Effect.gen(function* () { - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { - notification: { password_changed: { enabled: true, content_path: "missing.html" } }, - }, - }, - }); - expect( - (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) - .message, - ).toContain("Invalid config for auth.email.notification.password_changed.content_path: "); - }).pipe(Effect.provide(testLayer)), - ); - - it.live("resolves a relative notification content_path against the workdir", () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const templateDir = path.join(tempRoot.current, "supabase", "templates"); - yield* fs.makeDirectory(templateDir, { recursive: true }); - yield* fs.writeFileString(path.join(templateDir, "pw-changed.html"), ""); - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { - notification: { - password_changed: { - enabled: true, - content_path: "supabase/templates/pw-changed.html", - }, - }, - }, - }, - }); - yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - }).pipe(Effect.provide(testLayer)), - ); - - it("does not throw a disabled notification's missing content_path", () => { - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { - notification: { - password_changed: { enabled: false, content_path: "missing.html" }, - }, - }, - }, - }); - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)), - ).not.toThrow(); - }); - - it("does not throw a missing template content_path when auth is disabled", () => { - const config = baseConfig({ - auth: { enabled: false, email: { template: { invite: { content_path: "missing.html" } } } }, - }); - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)), - ).not.toThrow(); - }); - - it("rejects a template content key present without content_path", () => { - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { template: { invite: {} } }, - }, - }); - expect(() => - run( - resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current, undefined, { - auth: { email: { template: { invite: { content: "Hi" } } } }, - }), - ), - ).toThrow( - "Invalid config for auth.email.template.invite.content: please use content_path instead", - ); - }); - }); - - describe("auth.email.template/notification env overrides", () => { - // No raw-document presence gate needed here: `email.template`/`email.notification` are - // `Schema.Record`s, so the decoded config already reflects presence. - const tempRoot = useTempWorkdir("supabase-email-template-env-test-"); - - afterEach(() => { - delete testEnv["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT_PATH"]; - delete testEnv["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT"]; - delete testEnv["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_ENABLED"]; - delete testEnv["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_CONTENT_PATH"]; - delete testEnv["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_CONTENT"]; - }); - - it.live("lets an env-provided template content_path override a missing TOML content_path", () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - yield* fs.writeFileString(path.join(tempRoot.current, "invite.html"), ""); - testEnv["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT_PATH"] = "invite.html"; - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { template: { invite: {} } }, - }, - }); - yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - }).pipe(Effect.provide(testLayer)), - ); - - it.live("rejects a notification enabled only via env with a missing content_path file", () => - Effect.gen(function* () { - testEnv["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_ENABLED"] = "true"; - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { - notification: { password_changed: { enabled: false, content_path: "missing.html" } }, - }, - }, - }); - expect( - (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) - .message, - ).toContain("Invalid config for auth.email.notification.password_changed.content_path: "); - }).pipe(Effect.provide(testLayer)), - ); - - it("does not validate a notification disabled only via env despite a TOML-enabled section", () => { - testEnv["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_ENABLED"] = "false"; - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { - notification: { password_changed: { enabled: true, content_path: "missing.html" } }, - }, - }, - }); - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)), - ).not.toThrow(); - }); - - it.live( - "lets an env-provided notification content_path override a missing TOML content_path", - () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - yield* fs.writeFileString( - path.join(tempRoot.current, "pw-changed.html"), - "", - ); - testEnv["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_CONTENT_PATH"] = - "pw-changed.html"; - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { notification: { password_changed: { enabled: true } } }, - }, - }); - yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - }).pipe(Effect.provide(testLayer)), - ); - - it("rejects a template _CONTENT env override with no content_path configured", () => { - testEnv["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT"] = "Hi"; - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { template: { invite: {} } }, - }, - }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))).toThrow( - "Invalid config for auth.email.template.invite.content: please use content_path instead", - ); - }); - - it("rejects an enabled notification's _CONTENT env override with no content_path configured", () => { - testEnv["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_CONTENT"] = "Hi"; - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { notification: { password_changed: { enabled: true } } }, - }, - }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))).toThrow( - "Invalid config for auth.email.notification.password_changed.content: please use content_path instead", - ); - }); - - it("does not validate a disabled notification's _CONTENT env override", () => { - testEnv["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_CONTENT"] = "Hi"; - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { notification: { password_changed: { enabled: false } } }, - }, - }); - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)), - ).not.toThrow(); - }); - - it.live( - "lets a simultaneous template _CONTENT_PATH env override win over a _CONTENT env override", - () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - yield* fs.writeFileString(path.join(tempRoot.current, "invite.html"), ""); - testEnv["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT"] = "Hi"; - testEnv["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT_PATH"] = "invite.html"; - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { template: { invite: {} } }, - }, - }); - yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - }).pipe(Effect.provide(testLayer)), - ); - - it.live( - "preserves a remote block's valid template content_path over a missing-file ambient override", - () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - yield* fs.writeFileString(path.join(tempRoot.current, "invite.html"), ""); - testEnv["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT_PATH"] = "missing.html"; - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { template: { invite: { content_path: "invite.html" } } }, - }, - }); - yield* resolveLocalConfigValues( - config, - "127.0.0.1", - tempRoot.current, - undefined, - undefined, - new Set(["auth.email.template.invite.content_path"]), - ); - }).pipe(Effect.provide(testLayer)), - ); - - it.live( - "still applies a template _CONTENT_PATH override to a missing file when no remote block matched", - () => - Effect.gen(function* () { - testEnv["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT_PATH"] = "missing.html"; - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { template: { invite: { content_path: "invite.html" } } }, - }, - }); - expect( - (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) - .message, - ).toContain("Invalid config for auth.email.template.invite.content_path: "); - }).pipe(Effect.provide(testLayer)), - ); - - it.live( - "preserves a remote block's valid notification content_path over a missing-file ambient override", - () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - yield* fs.writeFileString( - path.join(tempRoot.current, "pw-changed.html"), - "", - ); - testEnv["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_CONTENT_PATH"] = - "missing.html"; - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { - notification: { - password_changed: { enabled: true, content_path: "pw-changed.html" }, - }, - }, - }, - }); - yield* resolveLocalConfigValues( - config, - "127.0.0.1", - tempRoot.current, - undefined, - undefined, - new Set(["auth.email.notification.password_changed.content_path"]), - ); - }).pipe(Effect.provide(testLayer)), - ); - - it("suppresses a malformed ambient notification _ENABLED when a remote block already set enabled", () => { - testEnv["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_ENABLED"] = "not-a-bool"; - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { notification: { password_changed: { enabled: false } } }, - }, - }); - expect(() => - run( - resolveLocalConfigValues( - config, - "127.0.0.1", - tempRoot.current, - undefined, - undefined, - new Set(["auth.email.notification.password_changed.enabled"]), - ), - ), - ).not.toThrow(); - }); - }); - - // Required-field/range assertions live in config-validate.unit.test.ts; only env-override - // mechanics are tested here. - - describe("auth.external (external.validate(), D-only, ported to L)", () => { - // Unmodeled external providers are silently dropped by the decoded config, so this reads the - // raw `document` (5th param) instead. - it("rejects an enabled unmodeled external provider missing client_id", () => { - const config = baseConfig(); - const document = { auth: { external: { custom: { enabled: true } } } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).toThrow("Missing required field in config: auth.external.custom.client_id"); - }); - - it("rejects an enabled unmodeled external provider missing secret", () => { - const config = baseConfig(); - const document = { - auth: { external: { custom: { enabled: true, client_id: "abc" } } }, - }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).toThrow("Missing required field in config: auth.external.custom.secret"); - }); - - it("does not require a secret for apple/google providers", () => { - const config = baseConfig(); - const document = { - auth: { external: { apple: { enabled: true, client_id: "abc" } } }, - }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).not.toThrow(); - }); - - it("skips deprecated linkedin/slack providers", () => { - const config = baseConfig(); - const document = { auth: { external: { slack: { enabled: true } } } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).not.toThrow(); - }); - - it("does not validate a disabled unmodeled external provider", () => { - const config = baseConfig(); - const document = { auth: { external: { custom: { enabled: false } } } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).not.toThrow(); - }); - - it("skips the check entirely when no document is threaded through", () => { - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - }); - - describe("auth.external env overrides", () => { - afterEach(() => { - delete testEnv["SUPABASE_AUTH_EXTERNAL_CUSTOM_ENABLED"]; - delete testEnv["SUPABASE_AUTH_EXTERNAL_CUSTOM_CLIENT_ID"]; - delete testEnv["SUPABASE_AUTH_EXTERNAL_CUSTOM_SECRET"]; - }); - - it("rejects a provider enabled only via env with no client_id", () => { - testEnv["SUPABASE_AUTH_EXTERNAL_CUSTOM_ENABLED"] = "true"; - const config = baseConfig(); - const document = { auth: { external: { custom: { enabled: false } } } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).toThrow("Missing required field in config: auth.external.custom.client_id"); - }); - - it("accepts env-provided client_id/secret overriding a TOML-enabled provider missing both", () => { - testEnv["SUPABASE_AUTH_EXTERNAL_CUSTOM_CLIENT_ID"] = "abc"; - testEnv["SUPABASE_AUTH_EXTERNAL_CUSTOM_SECRET"] = "shh"; - const config = baseConfig(); - const document = { auth: { external: { custom: { enabled: true } } } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).not.toThrow(); - }); - - it("does not synthesize a provider purely from an env override when the section is absent from the document", () => { - testEnv["SUPABASE_AUTH_EXTERNAL_CUSTOM_ENABLED"] = "true"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - }); - - describe("auth.sms env overrides (provider switch)", () => { - // Validates only the first enabled provider, in priority order (twilio, twilio_verify, - // messagebird, textlocal, vonage), re-run here against the raw document with env overrides - // applied. - afterEach(() => { - delete testEnv["SUPABASE_AUTH_SMS_TWILIO_ENABLED"]; - delete testEnv["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"]; - delete testEnv["SUPABASE_AUTH_SMS_TWILIO_MESSAGE_SERVICE_SID"]; - delete testEnv["SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN"]; - delete testEnv["SUPABASE_AUTH_SMS_MESSAGEBIRD_ENABLED"]; - }); - - it("rejects a provider enabled only via env with missing required fields", () => { - testEnv["SUPABASE_AUTH_SMS_TWILIO_ENABLED"] = "true"; - const config = baseConfig(); - const document = { auth: { sms: { twilio: { enabled: false } } } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).toThrow("Missing required field in config: auth.sms.twilio.account_sid"); - }); - - it("accepts env-provided credentials overriding a TOML-enabled provider missing them", () => { - testEnv["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"] = "AC123"; - testEnv["SUPABASE_AUTH_SMS_TWILIO_MESSAGE_SERVICE_SID"] = "MG123"; - testEnv["SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN"] = "tok"; - const config = baseConfig(); - const document = { auth: { sms: { twilio: { enabled: true } } } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).not.toThrow(); - }); - - it("only validates the first enabled provider in Go's fixed priority order", () => { - testEnv["SUPABASE_AUTH_SMS_TWILIO_ENABLED"] = "false"; - testEnv["SUPABASE_AUTH_SMS_MESSAGEBIRD_ENABLED"] = "true"; - const config = baseConfig(); - const document = { - auth: { sms: { twilio: { enabled: true }, messagebird: { enabled: false } } }, - }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).toThrow("Missing required field in config: auth.sms.messagebird.originator"); - }); - - it("throws for a provider enabled only via env with missing required fields even when the document has no auth.sms section at all", () => { - // Unlike the other 4 providers, twilio's presence isn't gated on the document — the default - // config always registers `auth.sms.twilio.*`, so enabling only via env still fails - // validation instead of doing nothing. - testEnv["SUPABASE_AUTH_SMS_TWILIO_ENABLED"] = "true"; - const config = baseConfig(); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Missing required field in config: auth.sms.twilio.account_sid", - ); - }); - - it("resolves a fully env-only twilio configuration with no auth.sms.twilio document section", () => { - testEnv["SUPABASE_AUTH_SMS_TWILIO_ENABLED"] = "true"; - testEnv["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"] = "AC123"; - testEnv["SUPABASE_AUTH_SMS_TWILIO_MESSAGE_SERVICE_SID"] = "MG123"; - testEnv["SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN"] = "tok"; - const resolved = run(resolveAuthSms(undefined, baseConfig().auth.sms, undefined)); - expect(resolved.twilio.enabled).toBe(true); - expect(resolved.twilio.account_sid).toBe("AC123"); - expect(resolved.twilio.message_service_sid).toBe("MG123"); - expect(resolved.twilio.auth_token).toBe("tok"); - }); - - it("still does not synthesize messagebird purely from an env override when the section is absent from the document", () => { - // Unlike twilio, messagebird has no entry in the default config template, so an absent - // section genuinely means it was never registered. - testEnv["SUPABASE_AUTH_SMS_MESSAGEBIRD_ENABLED"] = "true"; - const config = baseConfig(); - const resolved = run(resolveAuthSms(undefined, config.auth.sms, undefined)); - expect(resolved.messagebird.enabled).toBe(false); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); - }); - }); - - describe("resolveAuthSms (top-level scalars)", () => { - afterEach(() => { - delete testEnv["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"]; - delete testEnv["SUPABASE_AUTH_SMS_ENABLE_CONFIRMATIONS"]; - delete testEnv["SUPABASE_AUTH_SMS_MAX_FREQUENCY"]; - delete testEnv["SUPABASE_AUTH_SMS_TEMPLATE"]; - }); - - it("overrides enable_signup/enable_confirmations/max_frequency/template with no presence gate", () => { - testEnv["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"] = "true"; - testEnv["SUPABASE_AUTH_SMS_ENABLE_CONFIRMATIONS"] = "true"; - testEnv["SUPABASE_AUTH_SMS_MAX_FREQUENCY"] = "10s"; - testEnv["SUPABASE_AUTH_SMS_TEMPLATE"] = "Your OTP is {{ .Code }}"; - // A provider must be enabled, or `enable_signup` gets downgraded to false regardless of the - // override. - const configured = { - ...baseConfig().auth.sms, - twilio: { ...baseConfig().auth.sms.twilio, enabled: true }, - }; - const resolved = run(resolveAuthSms(undefined, configured, undefined)); - expect(resolved.enable_signup).toBe(true); - expect(resolved.enable_confirmations).toBe(true); - expect(resolved.max_frequency).toBe("10s"); - expect(resolved.template).toBe("Your OTP is {{ .Code }}"); - }); - - it("leaves the scalars at their configured values when nothing is overridden", () => { - const configured = { - ...baseConfig().auth.sms, - enable_signup: true, - max_frequency: "5s", - twilio: { ...baseConfig().auth.sms.twilio, enabled: true }, - }; - const resolved = run(resolveAuthSms(undefined, configured, undefined)); - expect(resolved.enable_signup).toBe(true); - expect(resolved.max_frequency).toBe("5s"); - }); - }); - - describe("resolveAuthSms (disables phone login with no provider enabled)", () => { - afterEach(() => { - delete testEnv["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"]; - }); - - it("downgrades enable_signup to false when configured true with no provider enabled", () => { - const configured = { ...baseConfig().auth.sms, enable_signup: true }; - const resolved = run(resolveAuthSms(undefined, configured, undefined)); - expect(resolved.enable_signup).toBe(false); - }); - - it("downgrades an env-overridden enable_signup to false with no provider enabled", () => { - testEnv["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"] = "true"; - const resolved = run(resolveAuthSms(undefined, baseConfig().auth.sms, undefined)); - expect(resolved.enable_signup).toBe(false); - }); - - it("leaves enable_signup alone when a provider is enabled", () => { - const configured = { - ...baseConfig().auth.sms, - enable_signup: true, - vonage: { ...baseConfig().auth.sms.vonage, enabled: true }, - }; - const resolved = run(resolveAuthSms(undefined, configured, undefined)); - expect(resolved.enable_signup).toBe(true); - }); - - it("leaves enable_signup at false when already false with no provider enabled", () => { - const resolved = run(resolveAuthSms(undefined, baseConfig().auth.sms, undefined)); - expect(resolved.enable_signup).toBe(false); - }); - }); - - describe("resolveAuthSms — remoteOverrideKeys (linked shadow provisioning, CLI-1956)", () => { - afterEach(() => { - delete testEnv["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"]; - delete testEnv["SUPABASE_AUTH_SMS_VONAGE_ENABLED"]; - delete testEnv["SUPABASE_AUTH_SMS_VONAGE_API_SECRET"]; - }); - - it("suppresses a malformed SUPABASE_AUTH_SMS_ENABLE_SIGNUP when a remote block already set auth.sms.enable_signup", () => { - testEnv["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"] = "not-a-bool"; - const configured = { - ...baseConfig().auth.sms, - enable_signup: true, - vonage: { ...baseConfig().auth.sms.vonage, enabled: true }, - }; - expect(() => - run(resolveAuthSms(undefined, configured, undefined, new Set(["auth.sms.enable_signup"]))), - ).not.toThrow(); - }); - - it("still rejects a malformed SUPABASE_AUTH_SMS_ENABLE_SIGNUP when no remote block matched", () => { - testEnv["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"] = "not-a-bool"; - const configured = { - ...baseConfig().auth.sms, - enable_signup: true, - vonage: { ...baseConfig().auth.sms.vonage, enabled: true }, - }; - expect(() => run(resolveAuthSms(undefined, configured, undefined))).toThrow( - 'cannot parse "not-a-bool" as a bool', - ); - }); - - it("suppresses a malformed SUPABASE_AUTH_SMS_VONAGE_ENABLED when a remote block already set auth.sms.vonage.enabled", () => { - testEnv["SUPABASE_AUTH_SMS_VONAGE_ENABLED"] = "not-a-bool"; - const configured = { - ...baseConfig().auth.sms, - vonage: { ...baseConfig().auth.sms.vonage, enabled: true }, - }; - expect(() => - run(resolveAuthSms(undefined, configured, undefined, new Set(["auth.sms.vonage.enabled"]))), - ).not.toThrow(); - }); - - it("prefers a remote-set auth.sms.vonage.api_secret over a malformed SUPABASE_AUTH_SMS_VONAGE_API_SECRET", () => { - testEnv["SUPABASE_AUTH_SMS_VONAGE_API_SECRET"] = "encrypted:garbage"; - const configured = { - ...baseConfig().auth.sms, - vonage: { ...baseConfig().auth.sms.vonage, enabled: true, api_secret: "remote-secret" }, - }; - const resolved = run( - resolveAuthSms( - undefined, - configured, - undefined, - new Set(["auth.sms.vonage.enabled", "auth.sms.vonage.api_secret"]), - ), - ); - expect(resolved.vonage.api_secret).toBe("remote-secret"); - }); - - it("still rejects a malformed SUPABASE_AUTH_SMS_VONAGE_API_SECRET when no remote block matched", () => { - testEnv["SUPABASE_AUTH_SMS_VONAGE_API_SECRET"] = "encrypted:garbage"; - const authDocument = { sms: { vonage: {} } }; - const configured = { - ...baseConfig().auth.sms, - vonage: { ...baseConfig().auth.sms.vonage, enabled: true, api_secret: "remote-secret" }, - }; - expect(() => run(resolveAuthSms(authDocument, configured, undefined))).toThrow( - "failed to parse config: missing private key", - ); - }); - - it("prefers a remote-set auth.sms.twilio.account_sid over a conflicting SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID", () => { - testEnv["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"] = "env-sid"; - const configured = { - ...baseConfig().auth.sms, - twilio: { ...baseConfig().auth.sms.twilio, account_sid: "remote-sid" }, - }; - const resolved = run( - resolveAuthSms(undefined, configured, undefined, new Set(["auth.sms.twilio.account_sid"])), - ); - expect(resolved.twilio.account_sid).toBe("remote-sid"); - delete testEnv["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"]; - }); - - it("still applies SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID when no remote block matched", () => { - testEnv["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"] = "env-sid"; - const configured = { - ...baseConfig().auth.sms, - twilio: { ...baseConfig().auth.sms.twilio, account_sid: "remote-sid" }, - }; - const resolved = run(resolveAuthSms(undefined, configured, undefined)); - expect(resolved.twilio.account_sid).toBe("env-sid"); - delete testEnv["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"]; - }); - - it("prefers a remote-set auth.sms.vonage.from over a conflicting SUPABASE_AUTH_SMS_VONAGE_FROM", () => { - testEnv["SUPABASE_AUTH_SMS_VONAGE_FROM"] = "env-from"; - const authDocument = { sms: { vonage: { from: "remote-from" } } }; - const configured = { - ...baseConfig().auth.sms, - vonage: { ...baseConfig().auth.sms.vonage, from: "remote-from" }, - }; - const resolved = run( - resolveAuthSms(authDocument, configured, undefined, new Set(["auth.sms.vonage.from"])), - ); - expect(resolved.vonage.from).toBe("remote-from"); - delete testEnv["SUPABASE_AUTH_SMS_VONAGE_FROM"]; - }); - - it("still applies SUPABASE_AUTH_SMS_VONAGE_FROM when no remote block matched", () => { - testEnv["SUPABASE_AUTH_SMS_VONAGE_FROM"] = "env-from"; - const authDocument = { sms: { vonage: { from: "remote-from" } } }; - const configured = { - ...baseConfig().auth.sms, - vonage: { ...baseConfig().auth.sms.vonage, from: "remote-from" }, - }; - const resolved = run(resolveAuthSms(authDocument, configured, undefined)); - expect(resolved.vonage.from).toBe("env-from"); - delete testEnv["SUPABASE_AUTH_SMS_VONAGE_FROM"]; - }); - - it("prefers a remote-set auth.sms.vonage.api_key over a conflicting SUPABASE_AUTH_SMS_VONAGE_API_KEY", () => { - testEnv["SUPABASE_AUTH_SMS_VONAGE_API_KEY"] = "env-key"; - const authDocument = { sms: { vonage: { api_key: "remote-key" } } }; - const configured = { - ...baseConfig().auth.sms, - vonage: { ...baseConfig().auth.sms.vonage, api_key: "remote-key" }, - }; - const resolved = run( - resolveAuthSms(authDocument, configured, undefined, new Set(["auth.sms.vonage.api_key"])), - ); - expect(resolved.vonage.api_key).toBe("remote-key"); - delete testEnv["SUPABASE_AUTH_SMS_VONAGE_API_KEY"]; - }); - - it("still applies SUPABASE_AUTH_SMS_VONAGE_API_KEY when no remote block matched", () => { - testEnv["SUPABASE_AUTH_SMS_VONAGE_API_KEY"] = "env-key"; - const authDocument = { sms: { vonage: { api_key: "remote-key" } } }; - const configured = { - ...baseConfig().auth.sms, - vonage: { ...baseConfig().auth.sms.vonage, api_key: "remote-key" }, - }; - const resolved = run(resolveAuthSms(authDocument, configured, undefined)); - expect(resolved.vonage.api_key).toBe("env-key"); - delete testEnv["SUPABASE_AUTH_SMS_VONAGE_API_KEY"]; - }); - - it("prefers a remote-set auth.sms.template over a conflicting SUPABASE_AUTH_SMS_TEMPLATE", () => { - testEnv["SUPABASE_AUTH_SMS_TEMPLATE"] = "env template"; - const configured = { ...baseConfig().auth.sms, template: "remote template" }; - const resolved = run( - resolveAuthSms(undefined, configured, undefined, new Set(["auth.sms.template"])), - ); - expect(resolved.template).toBe("remote template"); - delete testEnv["SUPABASE_AUTH_SMS_TEMPLATE"]; - }); - - it("still applies SUPABASE_AUTH_SMS_TEMPLATE when no remote block matched", () => { - testEnv["SUPABASE_AUTH_SMS_TEMPLATE"] = "env template"; - const configured = { ...baseConfig().auth.sms, template: "remote template" }; - const resolved = run(resolveAuthSms(undefined, configured, undefined)); - expect(resolved.template).toBe("env template"); - delete testEnv["SUPABASE_AUTH_SMS_TEMPLATE"]; - }); - - it("prefers a remote-set auth.sms.max_frequency over a conflicting SUPABASE_AUTH_SMS_MAX_FREQUENCY", () => { - testEnv["SUPABASE_AUTH_SMS_MAX_FREQUENCY"] = "5s"; - const configured = { ...baseConfig().auth.sms, max_frequency: "1m" }; - const resolved = run( - resolveAuthSms(undefined, configured, undefined, new Set(["auth.sms.max_frequency"])), - ); - expect(resolved.max_frequency).toBe("1m"); - delete testEnv["SUPABASE_AUTH_SMS_MAX_FREQUENCY"]; - }); - - it("still applies SUPABASE_AUTH_SMS_MAX_FREQUENCY when no remote block matched", () => { - testEnv["SUPABASE_AUTH_SMS_MAX_FREQUENCY"] = "5s"; - const configured = { ...baseConfig().auth.sms, max_frequency: "1m" }; - const resolved = run(resolveAuthSms(undefined, configured, undefined)); - expect(resolved.max_frequency).toBe("5s"); - delete testEnv["SUPABASE_AUTH_SMS_MAX_FREQUENCY"]; - }); - - it("still aborts resolveLocalConfigValues on a malformed SUPABASE_AUTH_SMS_ENABLE_SIGNUP reached via validateAuthSmsProviders, unless remoteOverrideKeys suppresses it", () => { - testEnv["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"] = "not-a-bool"; - const base = baseConfig(); - const config: CliConfig = { - ...base, - auth: { - ...base.auth, - enabled: true, - sms: { - ...base.auth.sms, - enable_signup: true, - vonage: { - ...base.auth.sms.vonage, - enabled: true, - from: "12345", - api_key: "key", - api_secret: "secret", - }, - }, - }, - }; - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - 'cannot parse "not-a-bool" as a bool', - ); - expect(() => - run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.sms.enable_signup"]), - ), - ), - ).not.toThrow(); + it("leaves session_replication_role undefined when neither configured nor overridden", () => { + expect(resolveDbSettingsEnvOverrides({}).session_replication_role).toBeUndefined(); }); }); - describe("api.tls (cert/key validation)", () => { - const tempRoot = useTempWorkdir("supabase-api-tls-test-"); + describe("auth.signing_keys_path (asymmetric JWT signing)", () => { + const tempRoot = useTempWorkdir("supabase-signing-keys-test-"); - const writeTlsFile = Effect.fnUntraced(function* ( - workdir: string, - name: string, - contents = "dummy", - ) { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const supabaseDir = path.join(workdir, "supabase"); - yield* fs.makeDirectory(supabaseDir, { recursive: true }); - yield* fs.writeFileString(path.join(supabaseDir, name), contents); - }); + it.live("signs anon/service_role with the first RS256 key in the file", () => + Effect.gen(function* () { + const jwk = generateRsaJwk(); + yield* writeSigningKeys(tempRoot.current, [jwk]); + const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - it("does not throw when tls.enabled with neither cert_path nor key_path set", () => { - const config = baseConfig({ api: { tls: { enabled: true } } }); - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)), - ).not.toThrow(); - }); + const publicJwk = { ...jwk, d: undefined, p: undefined, q: undefined, dp: undefined }; + const publicKey = yield* Effect.promise(() => importJWK(publicJwk, "RS256")); + const { payload, protectedHeader } = yield* Effect.promise(() => + jwtVerify(values.anonKey, publicKey), + ); + expect(payload).toMatchObject({ iss: "supabase-demo", role: "anon" }); + expect(protectedHeader).toMatchObject({ alg: "RS256", kid: "test-rsa-kid" }); - // The "exactly one of cert/key set" checks live in config-validate.unit.test.ts; the file-read - // behavior below is tested here. + const serviceRole = yield* Effect.promise(() => + jwtVerify(values.serviceRoleKey, publicKey), + ); + expect(serviceRole.payload).toMatchObject({ role: "service_role" }); + }).pipe(Effect.provide(BunServices.layer)), + ); - it.live("throws a Go-worded error when the configured cert file does not exist", () => + it.live("resolves a relative signing_keys_path against /supabase", () => Effect.gen(function* () { - yield* writeTlsFile(tempRoot.current, "key.pem"); - const config = baseConfig({ - api: { tls: { enabled: true, cert_path: "missing-cert.pem", key_path: "key.pem" } }, - }); - expect( - (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) - .message, - ).toContain("failed to read TLS cert: "); - }).pipe(Effect.provide(testLayer)), + const jwk = generateRsaJwk(); + yield* writeSigningKeys(tempRoot.current, [jwk]); + const config = baseConfig({ auth: { signing_keys_path: "./signing_keys.json" } }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + expect(values.anonKey.split(".")).toHaveLength(3); + }).pipe(Effect.provide(BunServices.layer)), ); - it.live("throws a Go-worded error when the configured key file does not exist", () => + it.live("uses an absolute signing_keys_path as-is, without joining the workdir", () => Effect.gen(function* () { - yield* writeTlsFile(tempRoot.current, "cert.pem"); - const config = baseConfig({ - api: { tls: { enabled: true, cert_path: "cert.pem", key_path: "missing-key.pem" } }, - }); - expect( - (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) - .message, - ).toContain("failed to read TLS key: "); - }).pipe(Effect.provide(testLayer)), + const jwk = generateRsaJwk(); + yield* writeSigningKeys(tempRoot.current, [jwk]); + const absolutePath = yield* join(tempRoot.current, "supabase", "signing_keys.json"); + const config = baseConfig({ auth: { signing_keys_path: absolutePath } }); + const values = yield* resolveLocalConfigValues( + config, + "127.0.0.1", + "/some/unrelated/workdir", + ); + expect(values.anonKey.split(".")).toHaveLength(3); + }).pipe(Effect.provide(BunServices.layer)), ); - it.live("succeeds when both cert_path and key_path are readable", () => + it.live("still prefers an explicit anon_key/service_role_key over signing keys", () => Effect.gen(function* () { - yield* writeTlsFile(tempRoot.current, "cert.pem"); - yield* writeTlsFile(tempRoot.current, "key.pem"); + yield* writeSigningKeys(tempRoot.current, [generateRsaJwk()]); const config = baseConfig({ - api: { tls: { enabled: true, cert_path: "cert.pem", key_path: "key.pem" } }, + auth: { + signing_keys_path: "signing_keys.json", + anon_key: "configured-anon", + service_role_key: "configured-service-role", + }, }); - yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - }).pipe(Effect.provide(testLayer)), - ); - - it.live( - "resolves cert_path/key_path against /supabase unconditionally, no isAbsolute guard", - () => - Effect.gen(function* () { - yield* writeTlsFile(tempRoot.current, "cert.pem"); - yield* writeTlsFile(tempRoot.current, "key.pem"); - const config = baseConfig({ - api: { - tls: { - enabled: true, - cert_path: "/cert.pem", - key_path: "/key.pem", - }, - }, - }); - yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - }).pipe(Effect.provide(testLayer)), + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + expect(values.anonKey).toBe("configured-anon"); + expect(values.serviceRoleKey).toBe("configured-service-role"); + }).pipe(Effect.provide(BunServices.layer)), ); - it("skips TLS validation entirely when api is disabled", () => { - const config = baseConfig({ - api: { enabled: false, tls: { enabled: true, cert_path: "missing-cert.pem" } }, - }); - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)), - ).not.toThrow(); - }); - - describe("SUPABASE_API_ENABLED / SUPABASE_API_TLS_ENABLED env overrides", () => { - afterEach(() => { - delete testEnv["SUPABASE_API_ENABLED"]; - delete testEnv["SUPABASE_API_TLS_ENABLED"]; - }); - - it("skips TLS validation when api is disabled only via env", () => { - testEnv["SUPABASE_API_ENABLED"] = "false"; - const config = baseConfig({ - api: { enabled: true, tls: { enabled: true, cert_path: "missing-cert.pem" } }, - }); - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)), - ).not.toThrow(); - }); - - it("validates TLS when enabled only via env despite TOML saying tls.enabled = false", () => { - testEnv["SUPABASE_API_TLS_ENABLED"] = "true"; - const config = baseConfig({ - api: { tls: { enabled: false, cert_path: "missing-cert.pem" } }, - }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))).toThrow( - "Missing required field in config: api.tls.key_path", - ); - }); - }); - }); -}); - -describe("resolveLocalConfigValues — remoteOverrideKeys (linked shadow provisioning, CLI-1956)", () => { - // Once a remote block sets a field, a conflicting `SUPABASE_*` env var must not be consulted - // for it — verified per field below. - afterEach(() => { - for (const name of [ - "SUPABASE_DB_MAJOR_VERSION", - "SUPABASE_AUTH_JWT_SECRET", - "SUPABASE_DB_ROOT_KEY", - "SUPABASE_API_PORT", - "SUPABASE_API_TLS_ENABLED", - "SUPABASE_API_EXTERNAL_URL", - "SUPABASE_DB_PORT", - "SUPABASE_AUTH_SITE_URL", - "SUPABASE_AUTH_JWT_EXPIRY", - "SUPABASE_AUTH_ANON_KEY", - "SUPABASE_AUTH_SERVICE_ROLE_KEY", - "SUPABASE_STUDIO_API_URL", - "SUPABASE_STUDIO_OPENAI_API_KEY", - "SUPABASE_AUTH_PUBLISHABLE_KEY", - "SUPABASE_AUTH_SECRET_KEY", - "SUPABASE_DB_SETTINGS_MAX_CONNECTIONS", - "SUPABASE_AUTH_SIGNING_KEYS_PATH", - "SUPABASE_AUTH_ENABLED", - "SUPABASE_ANALYTICS_ENABLED", - "SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED", - "SUPABASE_AUTH_THIRD_PARTY_CLERK_ENABLED", - "SUPABASE_AUTH_THIRD_PARTY_CLERK_DOMAIN", - "SUPABASE_EDGE_RUNTIME_DENO_VERSION", - "SUPABASE_API_ENABLED", - "SUPABASE_STUDIO_ENABLED", - "SUPABASE_STUDIO_PORT", - "SUPABASE_LOCAL_SMTP_ENABLED", - "SUPABASE_LOCAL_SMTP_PORT", - "SUPABASE_AUTH_ENABLE_SIGNUP", - "SUPABASE_AUTH_ENABLE_ANONYMOUS_SIGN_INS", - "SUPABASE_AUTH_ENABLE_REFRESH_TOKEN_ROTATION", - "SUPABASE_AUTH_REFRESH_TOKEN_REUSE_INTERVAL", - "SUPABASE_AUTH_ENABLE_MANUAL_LINKING", - "SUPABASE_AUTH_MINIMUM_PASSWORD_LENGTH", - "SUPABASE_AUTH_PASSWORD_REQUIREMENTS", - "SUPABASE_AUTH_PASSKEY_ENABLED", - "SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED", - "SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI", - ]) { - delete testEnv[name]; - } - }); - - const tempRoot = useTempWorkdir("supabase-remote-signing-keys-test-"); - - it.live( - "prefers a remote-set auth.signing_keys_path over a conflicting SUPABASE_AUTH_SIGNING_KEYS_PATH", - () => + it.live("falls back to HMAC signing when signing_keys_path resolves to an empty array", () => Effect.gen(function* () { - yield* writeSigningKeys(tempRoot.current, [generateRsaJwk()]); - testEnv["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "missing-file.json"; + yield* writeSigningKeys(tempRoot.current, []); const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - yield* resolveLocalConfigValues( - config, - "127.0.0.1", - tempRoot.current, - undefined, - undefined, - new Set(["auth.signing_keys_path"]), + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + const [, payload] = values.anonKey.split("."); + expect(yield* decodeJson(Buffer.from(payload ?? "", "base64url").toString())).toMatchObject( + { + iss: "supabase-demo", + }, ); - }).pipe(Effect.provide(testLayer)), - ); + }).pipe(Effect.provide(BunServices.layer)), + ); - it.live( - "still rejects a missing SUPABASE_AUTH_SIGNING_KEYS_PATH override when no remote block matched", - () => + it.live("throws an error when the signing keys file does not exist", () => Effect.gen(function* () { - yield* writeSigningKeys(tempRoot.current, [generateRsaJwk()]); - testEnv["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "missing-file.json"; - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + const config = baseConfig({ auth: { signing_keys_path: "missing.json" } }); expect( (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) .message, ).toContain("failed to read signing keys: "); - }).pipe(Effect.provide(testLayer)), - ); - - it("suppresses a malformed SUPABASE_DB_MAJOR_VERSION when a remote block already set db.major_version", () => { - testEnv["SUPABASE_DB_MAJOR_VERSION"] = "abc"; - const config = baseConfig({ db: { major_version: 14 } }); - expect(() => - run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["db.major_version"]), - ), - ), - ).not.toThrow(); - }); - - it("still rejects a malformed SUPABASE_DB_MAJOR_VERSION when no remote block matched", () => { - testEnv["SUPABASE_DB_MAJOR_VERSION"] = "abc"; - const config = baseConfig({ db: { major_version: 14 } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Invalid db.major_version: abc", + }).pipe(Effect.provide(BunServices.layer)), ); - }); - it("prefers a remote-set auth.jwt_secret over a conflicting SUPABASE_AUTH_JWT_SECRET", () => { - testEnv["SUPABASE_AUTH_JWT_SECRET"] = "env-supplied-secret-value-1234567890"; - const config = baseConfig({ auth: { jwt_secret: "remote-supplied-secret-1234567890" } }); - const values = run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.jwt_secret"]), - ), + it.live("throws an error when the signing keys file is malformed JSON", () => + Effect.gen(function* () { + const supabaseDir = yield* join(tempRoot.current, "supabase"); + yield* mkdirp(supabaseDir); + yield* writeFile(yield* join(supabaseDir, "signing_keys.json"), "not valid json"); + const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) + .message, + ).toContain('failed to decode signing keys: JSON Parse error: Unexpected identifier "not"'); + }).pipe(Effect.provide(BunServices.layer)), ); - expect(values.jwtSecret).toBe("remote-supplied-secret-1234567890"); - }); - it("prefers a remote-set db.root_key over a conflicting SUPABASE_DB_ROOT_KEY", () => { - testEnv["SUPABASE_DB_ROOT_KEY"] = "env-root-key"; - const config = baseConfig(); - const document = { db: { root_key: "remote-root-key" } }; - const values = run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - document, - new Set(["db.root_key"]), - ), + it.live("throws when the first key uses an unsupported algorithm", () => + Effect.gen(function* () { + yield* writeSigningKeys(tempRoot.current, [{ ...generateRsaJwk(), alg: "RS512" }]); + const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) + .message, + ).toContain("unsupported algorithm: RS512"); + }).pipe(Effect.provide(BunServices.layer)), ); - expect(values.rootKey).toBe("remote-root-key"); - }); - it("prefers a remote-set auth.third_party.clerk.domain over a conflicting env override during validation", () => { - testEnv["SUPABASE_AUTH_THIRD_PARTY_CLERK_ENABLED"] = "false"; - testEnv["SUPABASE_AUTH_THIRD_PARTY_CLERK_DOMAIN"] = "not-a-clerk-domain"; - const config = baseConfig({ - auth: { - enabled: true, - third_party: { clerk: { enabled: true, domain: "clerk.example.com" } }, - }, - }); - expect(() => - run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.third_party.clerk.enabled", "auth.third_party.clerk.domain"]), - ), - ), - ).not.toThrow(); - }); + it.live("skips reading a missing signing_keys_path when auth is disabled", () => + Effect.gen(function* () { + const config = baseConfig({ + auth: { enabled: false, signing_keys_path: "missing.json" }, + }); + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("still rejects a conflicting SUPABASE_AUTH_THIRD_PARTY_CLERK_DOMAIN when no remote block matched", () => { - testEnv["SUPABASE_AUTH_THIRD_PARTY_CLERK_DOMAIN"] = "not-a-clerk-domain"; - const config = baseConfig({ - auth: { - enabled: true, - third_party: { clerk: { enabled: true, domain: "clerk.example.com" } }, - }, - }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Invalid config: auth.third_party.clerk has invalid domain", + it.live( + "skips reading a malformed signing_keys_path when auth is disabled, but still signs asymmetrically with the default key", + () => + Effect.gen(function* () { + const supabaseDir = yield* join(tempRoot.current, "supabase"); + yield* mkdirp(supabaseDir); + yield* writeFile(yield* join(supabaseDir, "signing_keys.json"), "not valid json"); + const config = baseConfig({ + auth: { enabled: false, signing_keys_path: "signing_keys.json" }, + }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + // Disabled auth with a configured signing-keys path still signs with the default ES256 key, + // not HMAC — signing depends on whether keys were loaded, not on auth being enabled. + const publicKey = yield* Effect.promise(() => + importJWK({ ...DEFAULT_SIGNING_KEY, d: undefined, key_ops: undefined }, "ES256"), + ); + const { payload, protectedHeader } = yield* Effect.promise(() => + jwtVerify(values.anonKey, publicKey), + ); + expect(payload).toMatchObject({ iss: "supabase-demo", role: "anon" }); + expect(protectedHeader).toMatchObject({ alg: "ES256", kid: DEFAULT_SIGNING_KEY.kid }); + }).pipe(Effect.provide(BunServices.layer)), ); }); - describe("api.tls.cert_path/key_path — remoteOverrideKeys (review: PRRT_kwDOErm0O86W8ZYk)", () => { - const tempRoot = useTempWorkdir("supabase-api-tls-remote-test-"); - - const writeTlsFile = Effect.fnUntraced(function* ( - workdir: string, - name: string, - contents = "dummy", - ) { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const supabaseDir = path.join(workdir, "supabase"); - yield* fs.makeDirectory(supabaseDir, { recursive: true }); - yield* fs.writeFileString(path.join(supabaseDir, name), contents); - }); + // Required-field/range assertions live in config-validate.unit.test.ts; only env-override + // mechanics are tested here. - afterEach(() => { - delete testEnv["SUPABASE_API_TLS_CERT_PATH"]; - delete testEnv["SUPABASE_API_TLS_KEY_PATH"]; - }); + describe("auth.passkey / auth.webauthn env overrides", () => { + // `auth.passkey`/`auth.webauthn` have no decoded-schema presence signal, so these tests thread + // a raw `document` object through explicitly instead of relying on `baseConfig`. it.live( - "prefers a remote-set api.tls.cert_path/key_path over a conflicting (missing-file) env override", + "throws on an unparsable raw auth.passkey.enabled string instead of silently disabling it", () => Effect.gen(function* () { - // The env vars point at files that don't exist; the load only succeeds if the remote-set - // paths win instead. - yield* writeTlsFile(tempRoot.current, "cert.pem"); - yield* writeTlsFile(tempRoot.current, "key.pem"); - testEnv["SUPABASE_API_TLS_CERT_PATH"] = "missing-cert.pem"; - testEnv["SUPABASE_API_TLS_KEY_PATH"] = "missing-key.pem"; - const config = baseConfig({ - api: { tls: { enabled: true, cert_path: "cert.pem", key_path: "key.pem" } }, - }); - yield* resolveLocalConfigValues( - config, - "127.0.0.1", - tempRoot.current, - undefined, - undefined, - new Set(["api.tls.cert_path", "api.tls.key_path"]), - ); - }).pipe(Effect.provide(testLayer)), + const config = baseConfig(); + const document = { auth: { passkey: { enabled: "not-a-bool" } } }; + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document))) + .message, + ).toContain('"not-a-bool" is not a valid boolean'); + }).pipe(Effect.provide(BunServices.layer)), ); + }); + + describe("auth.email.template/notification (content_path validation)", () => { + const tempRoot = useTempWorkdir("supabase-email-templates-test-"); - it.live("still uses the env override when no remote block matched", () => + it.live("rejects a template content_path pointing at a missing file", () => Effect.gen(function* () { - yield* writeTlsFile(tempRoot.current, "cert.pem"); - testEnv["SUPABASE_API_TLS_CERT_PATH"] = "missing-cert.pem"; const config = baseConfig({ - api: { tls: { enabled: true, cert_path: "cert.pem", key_path: "cert.pem" } }, + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { template: { invite: { content_path: "missing-invite.html" } } }, + }, }); expect( (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) .message, - ).toContain("failed to read TLS cert: "); - }).pipe(Effect.provide(testLayer)), - ); - }); - - it("prefers remote-set api.port/api.tls.enabled/api.external_url over conflicting env overrides", () => { - testEnv["SUPABASE_API_PORT"] = "9999"; - testEnv["SUPABASE_API_TLS_ENABLED"] = "true"; - testEnv["SUPABASE_API_EXTERNAL_URL"] = "https://env-should-not-win.test"; - const config = baseConfig({ api: { port: 54321, external_url: "", tls: { enabled: false } } }); - const values = run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["api.port", "api.tls.enabled", "api.external_url"]), - ), - ); - expect(values.apiUrl).toBe("http://127.0.0.1:54321"); - }); - - it("prefers a remote-set db.port over a conflicting SUPABASE_DB_PORT", () => { - testEnv["SUPABASE_DB_PORT"] = "9999"; - const config = baseConfig({ db: { port: 54322 } }); - const values = run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["db.port"]), - ), + ).toContain("Invalid config for auth.email.template.invite.content_path: "); + }).pipe(Effect.provide(BunServices.layer)), ); - expect(values.dbPort).toBe(54322); - expect(values.dbUrl).toContain(":54322/postgres"); - }); - it("prefers remote-set auth.site_url/auth.jwt_expiry over conflicting env overrides", () => { - testEnv["SUPABASE_AUTH_SITE_URL"] = "https://env-should-not-win.test"; - testEnv["SUPABASE_AUTH_JWT_EXPIRY"] = "9999"; - const config = baseConfig({ auth: { site_url: "https://remote.test", jwt_expiry: 3600 } }); - const values = run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.site_url", "auth.jwt_expiry"]), - ), + it.live("rejects an absolute template content_path outside the project root", () => + Effect.gen(function* () { + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { template: { invite: { content_path: "/etc/hosts" } } }, + }, + }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) + .message, + ).toContain( + 'Invalid config for auth.email.template.invite.content_path: "/etc/hosts" resolves outside the project root', + ); + }).pipe(Effect.provide(BunServices.layer)), ); - expect(values.authSiteUrl).toBe("https://remote.test"); - expect(values.authJwtExpiry).toBe(3600); - }); - it("prefers remote-set auth.anon_key/auth.service_role_key over conflicting env overrides", () => { - testEnv["SUPABASE_AUTH_ANON_KEY"] = "env-anon-key"; - testEnv["SUPABASE_AUTH_SERVICE_ROLE_KEY"] = "env-service-role-key"; - const config = baseConfig({ - auth: { anon_key: "remote-anon-key", service_role_key: "remote-service-role-key" }, - }); - const values = run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.anon_key", "auth.service_role_key"]), - ), + it.live( + "resolves a relative template content_path against the workdir itself, not /supabase", + () => + Effect.gen(function* () { + yield* writeFile(yield* join(tempRoot.current, "invite.html"), ""); + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { template: { invite: { content_path: "invite.html" } } }, + }, + }); + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), ); - expect(values.anonKey).toBe("remote-anon-key"); - expect(values.serviceRoleKey).toBe("remote-service-role-key"); - }); - it("suppresses a malformed SUPABASE_STUDIO_API_URL when a remote block already set studio.api_url", () => { - testEnv["SUPABASE_STUDIO_API_URL"] = "http://[::1"; - const config = baseConfig({ studio: { api_url: "http://remote.test" } }); - expect(() => - run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["studio.api_url"]), - ), - ), - ).not.toThrow(); - }); - - it("prefers a remote-set studio.openai_api_key over a conflicting SUPABASE_STUDIO_OPENAI_API_KEY", () => { - testEnv["SUPABASE_STUDIO_OPENAI_API_KEY"] = "encrypted:not-a-real-ciphertext"; - const config = baseConfig({ studio: { openai_api_key: "remote-openai-key" } }); - const values = run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["studio.openai_api_key"]), - ), + it.live("does not throw a template with no content_path configured", () => + Effect.gen(function* () { + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { template: { invite: {} } }, + }, + }); + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), ); - expect(values.openaiApiKey).toBe("remote-openai-key"); - }); - it("still rejects a malformed SUPABASE_STUDIO_OPENAI_API_KEY when no remote block matched", () => { - testEnv["SUPABASE_STUDIO_OPENAI_API_KEY"] = "encrypted:not-a-real-ciphertext"; - const config = baseConfig({ studio: { openai_api_key: "remote-openai-key" } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "failed to parse config: missing private key", + it.live("rejects an enabled notification content_path pointing at a missing file", () => + Effect.gen(function* () { + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { + notification: { password_changed: { enabled: true, content_path: "missing.html" } }, + }, + }, + }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) + .message, + ).toContain("Invalid config for auth.email.notification.password_changed.content_path: "); + }).pipe(Effect.provide(BunServices.layer)), ); - }); - it("prefers remote-set auth.publishable_key/auth.secret_key over conflicting env overrides", () => { - testEnv["SUPABASE_AUTH_PUBLISHABLE_KEY"] = "encrypted:not-a-real-ciphertext"; - testEnv["SUPABASE_AUTH_SECRET_KEY"] = "encrypted:not-a-real-ciphertext"; - const config = baseConfig({ - auth: { publishable_key: "remote-publishable-key", secret_key: "remote-secret-key" }, - }); - const values = run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.publishable_key", "auth.secret_key"]), - ), + it.live("resolves a relative notification content_path against the workdir", () => + Effect.gen(function* () { + const templateDir = yield* join(tempRoot.current, "supabase", "templates"); + yield* mkdirp(templateDir); + yield* writeFile(yield* join(templateDir, "pw-changed.html"), ""); + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { + notification: { + password_changed: { + enabled: true, + content_path: "supabase/templates/pw-changed.html", + }, + }, + }, + }, + }); + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), ); - expect(values.publishableKey).toBe("remote-publishable-key"); - expect(values.secretKey).toBe("remote-secret-key"); - }); - it("still rejects a malformed SUPABASE_AUTH_PUBLISHABLE_KEY when no remote block matched", () => { - testEnv["SUPABASE_AUTH_PUBLISHABLE_KEY"] = "encrypted:not-a-real-ciphertext"; - const config = baseConfig({ auth: { publishable_key: "remote-publishable-key" } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "failed to parse config: missing private key", + it.live("does not throw a disabled notification's missing content_path", () => + Effect.gen(function* () { + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { + notification: { + password_changed: { enabled: false, content_path: "missing.html" }, + }, + }, + }, + }); + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), ); - }); - it("still rejects a malformed SUPABASE_AUTH_SECRET_KEY when no remote block matched", () => { - testEnv["SUPABASE_AUTH_SECRET_KEY"] = "encrypted:not-a-real-ciphertext"; - const config = baseConfig({ auth: { secret_key: "remote-secret-key" } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "failed to parse config: missing private key", + it.live("does not throw a missing template content_path when auth is disabled", () => + Effect.gen(function* () { + const config = baseConfig({ + auth: { + enabled: false, + email: { template: { invite: { content_path: "missing.html" } } }, + }, + }); + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), ); - }); - - it("suppresses a malformed SUPABASE_DB_SETTINGS_MAX_CONNECTIONS when the remote block set db.settings.max_connections", () => { - testEnv["SUPABASE_DB_SETTINGS_MAX_CONNECTIONS"] = "not-a-number"; - const config = baseConfig({ db: { settings: { max_connections: 100 } } }); - expect(() => - run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["db.settings.max_connections"]), - ), - ), - ).not.toThrow(); - }); - it.live( - "suppresses a malformed SUPABASE_AUTH_ENABLED when a remote block already set auth.enabled", - () => + it.live("rejects a template content key present without content_path", () => Effect.gen(function* () { - testEnv["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; - const config = baseConfig({ auth: { enabled: false } }); - yield* resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.enabled"]), + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { template: { invite: {} } }, + }, + }); + expect( + (yield* Effect.flip( + resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current, { + auth: { email: { template: { invite: { content: "Hi" } } } }, + }), + )).message, + ).toContain( + "Invalid config for auth.email.template.invite.content: please use content_path instead", ); - }).pipe(Effect.provide(testLayer)), - ); - - it.live("still rejects a malformed SUPABASE_AUTH_ENABLED when no remote block matched", () => - Effect.gen(function* () { - testEnv["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; - const config = baseConfig({ auth: { enabled: false } }); - expect( - (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).message, - ).toContain('Invalid config for auth.enabled: cannot parse "not-a-bool" as a bool'); - }).pipe(Effect.provide(testLayer)), - ); - - it("suppresses a malformed SUPABASE_ANALYTICS_ENABLED when a remote block already set analytics.enabled", () => { - testEnv["SUPABASE_ANALYTICS_ENABLED"] = "not-a-bool"; - const config = baseConfig({ analytics: { enabled: false } }); - expect(() => - run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["analytics.enabled"]), - ), - ), - ).not.toThrow(); - }); - - it("still rejects a malformed SUPABASE_ANALYTICS_ENABLED when no remote block matched", () => { - testEnv["SUPABASE_ANALYTICS_ENABLED"] = "not-a-bool"; - const config = baseConfig({ analytics: { enabled: false } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - 'Invalid config for analytics.enabled: cannot parse "not-a-bool" as a bool', - ); - }); - - it("prefers a remote-set analytics.gcp_project_id over a conflicting SUPABASE_ANALYTICS_GCP_PROJECT_ID", () => { - testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_ID"] = "env-project"; - const config = baseConfig({ analytics: { gcp_project_id: "remote-project" } }); - const values = run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["analytics.gcp_project_id"]), - ), + }).pipe(Effect.provide(BunServices.layer)), ); - expect(values.gcpProjectId).toBe("remote-project"); - delete testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_ID"]; }); - it("still applies SUPABASE_ANALYTICS_GCP_PROJECT_ID when no remote block matched", () => { - testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_ID"] = "env-project"; - const config = baseConfig({ analytics: { gcp_project_id: "remote-project" } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.gcpProjectId).toBe("env-project"); - delete testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_ID"]; - }); + // Required-field/range assertions live in config-validate.unit.test.ts; only env-override + // mechanics are tested here. - it("prefers a remote-set analytics.gcp_project_number over a conflicting SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER", () => { - testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER"] = "999"; - const config = baseConfig({ analytics: { gcp_project_number: "111" } }); - const values = run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["analytics.gcp_project_number"]), - ), + describe("auth.external (external.validate(), D-only, ported to L)", () => { + // Unmodeled external providers are silently dropped by the decoded config, so this reads the + // raw `document` (5th param) instead. + it.live("rejects an enabled unmodeled external provider missing client_id", () => + Effect.gen(function* () { + const config = baseConfig(); + const document = { auth: { external: { custom: { enabled: true } } } }; + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document))) + .message, + ).toContain("Missing required field in config: auth.external.custom.client_id"); + }).pipe(Effect.provide(BunServices.layer)), ); - expect(values.gcpProjectNumber).toBe("111"); - delete testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER"]; - }); - - it("still applies SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER when no remote block matched", () => { - testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER"] = "999"; - const config = baseConfig({ analytics: { gcp_project_number: "111" } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.gcpProjectNumber).toBe("999"); - delete testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER"]; - }); - it("prefers a remote-set analytics.gcp_jwt_path over a conflicting SUPABASE_ANALYTICS_GCP_JWT_PATH", () => { - testEnv["SUPABASE_ANALYTICS_GCP_JWT_PATH"] = "env-key.json"; - const config = baseConfig({ analytics: { gcp_jwt_path: "remote-key.json" } }); - const values = run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["analytics.gcp_jwt_path"]), - ), + it.live("rejects an enabled unmodeled external provider missing secret", () => + Effect.gen(function* () { + const config = baseConfig(); + const document = { + auth: { external: { custom: { enabled: true, client_id: "abc" } } }, + }; + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document))) + .message, + ).toContain("Missing required field in config: auth.external.custom.secret"); + }).pipe(Effect.provide(BunServices.layer)), ); - expect(values.gcpJwtPath).toBe("remote-key.json"); - delete testEnv["SUPABASE_ANALYTICS_GCP_JWT_PATH"]; - }); - - it("still applies SUPABASE_ANALYTICS_GCP_JWT_PATH when no remote block matched", () => { - testEnv["SUPABASE_ANALYTICS_GCP_JWT_PATH"] = "env-key.json"; - const config = baseConfig({ analytics: { gcp_jwt_path: "remote-key.json" } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.gcpJwtPath).toBe("env-key.json"); - delete testEnv["SUPABASE_ANALYTICS_GCP_JWT_PATH"]; - }); - it("prefers a remote-set auth.jwt_issuer over a conflicting SUPABASE_AUTH_JWT_ISSUER", () => { - testEnv["SUPABASE_AUTH_JWT_ISSUER"] = "https://env.example.com"; - const config = baseConfig({ auth: { jwt_issuer: "https://remote.example.com" } }); - const values = run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.jwt_issuer"]), - ), + it.live("does not require a secret for apple/google providers", () => + Effect.gen(function* () { + const config = baseConfig(); + const document = { + auth: { external: { apple: { enabled: true, client_id: "abc" } } }, + }; + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), ); - expect(values.authJwtIssuer).toBe("https://remote.example.com"); - delete testEnv["SUPABASE_AUTH_JWT_ISSUER"]; - }); - - it("still applies SUPABASE_AUTH_JWT_ISSUER when no remote block matched", () => { - testEnv["SUPABASE_AUTH_JWT_ISSUER"] = "https://env.example.com"; - const config = baseConfig({ auth: { jwt_issuer: "https://remote.example.com" } }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.authJwtIssuer).toBe("https://env.example.com"); - delete testEnv["SUPABASE_AUTH_JWT_ISSUER"]; - }); - it("prefers a remote-set auth.additional_redirect_urls over a conflicting SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS", () => { - testEnv["SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS"] = "https://env.example.com"; - const config = baseConfig({ - auth: { additional_redirect_urls: ["https://remote.example.com"] }, - }); - const values = run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.additional_redirect_urls"]), - ), + it.live("does not validate a disabled unmodeled external provider", () => + Effect.gen(function* () { + const config = baseConfig(); + const document = { auth: { external: { custom: { enabled: false } } } }; + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), ); - expect(values.authAdditionalRedirectUrls).toEqual(["https://remote.example.com"]); - delete testEnv["SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS"]; - }); - it("still applies SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS when no remote block matched", () => { - testEnv["SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS"] = "https://env.example.com"; - const config = baseConfig({ - auth: { additional_redirect_urls: ["https://remote.example.com"] }, - }); - const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); - expect(values.authAdditionalRedirectUrls).toEqual(["https://env.example.com"]); - delete testEnv["SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS"]; + it.live("skips the check entirely when no document is threaded through", () => + Effect.gen(function* () { + const config = baseConfig(); + expect(yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); }); - describe("auth.webauthn.rp_id / auth.webauthn.rp_origins — remoteOverrideKeys (linked shadow provisioning, CLI-1956)", () => { - // rpId/rpOrigins aren't part of the return value, so precedence is proven through - // validateResolvedConfig's own emptiness check on a document that leaves the field present - // but empty. - afterEach(() => { - delete testEnv["SUPABASE_AUTH_PASSKEY_ENABLED"]; - delete testEnv["SUPABASE_AUTH_WEBAUTHN_RP_ID"]; - delete testEnv["SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS"]; - }); - - it("suppresses a non-empty SUPABASE_AUTH_WEBAUTHN_RP_ID when a remote block already set (empty) auth.webauthn.rp_id", () => { - testEnv["SUPABASE_AUTH_WEBAUTHN_RP_ID"] = "localhost"; - const config = baseConfig(); - const document = { - auth: { passkey: { enabled: true }, webauthn: { rp_id: "", rp_origins: ["http://x"] } }, - }; - expect(() => - run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - document, - new Set(["auth.webauthn.rp_id"]), - ), - ), - ).toThrow("Missing required field in config: auth.webauthn.rp_id"); - }); - - it("still applies SUPABASE_AUTH_WEBAUTHN_RP_ID when no remote block matched", () => { - testEnv["SUPABASE_AUTH_WEBAUTHN_RP_ID"] = "localhost"; - const config = baseConfig(); - const document = { - auth: { passkey: { enabled: true }, webauthn: { rp_id: "", rp_origins: ["http://x"] } }, + describe("resolveAuthSms (top-level scalars)", () => { + it("leaves the scalars at their configured values when nothing is overridden", () => { + const configured = { + ...baseConfig().auth.sms, + enable_signup: true, + max_frequency: "5s", + twilio: { ...baseConfig().auth.sms.twilio, enabled: true }, }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).not.toThrow(); + const resolved = resolveAuthSms(configured); + expect(resolved.enable_signup).toBe(true); + expect(resolved.max_frequency).toBe("5s"); }); + }); - it("suppresses a non-empty SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS when a remote block already set (empty) auth.webauthn.rp_origins", () => { - testEnv["SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS"] = "http://localhost:3000"; - const config = baseConfig(); - const document = { - auth: { passkey: { enabled: true }, webauthn: { rp_id: "localhost", rp_origins: [] } }, - }; - expect(() => - run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - document, - new Set(["auth.webauthn.rp_origins"]), - ), - ), - ).toThrow("Missing required field in config: auth.webauthn.rp_origins"); + describe("resolveAuthSms (disables phone login with no provider enabled)", () => { + it("downgrades enable_signup to false when configured true with no provider enabled", () => { + const configured = { ...baseConfig().auth.sms, enable_signup: true }; + const resolved = resolveAuthSms(configured); + expect(resolved.enable_signup).toBe(false); }); - it("still applies SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS when no remote block matched", () => { - testEnv["SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS"] = "http://localhost:3000"; - const config = baseConfig(); - const document = { - auth: { passkey: { enabled: true }, webauthn: { rp_id: "localhost", rp_origins: [] } }, + it("leaves enable_signup alone when a provider is enabled", () => { + const configured = { + ...baseConfig().auth.sms, + enable_signup: true, + vonage: { ...baseConfig().auth.sms.vonage, enabled: true }, }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).not.toThrow(); + const resolved = resolveAuthSms(configured); + expect(resolved.enable_signup).toBe(true); }); - }); - it("suppresses a malformed SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED when a remote block already set auth.third_party.firebase.enabled", () => { - testEnv["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED"] = "not-a-bool"; - const config = baseConfig({ - auth: { enabled: true, third_party: { firebase: { enabled: false } } }, + it("leaves enable_signup at false when already false with no provider enabled", () => { + const resolved = resolveAuthSms(baseConfig().auth.sms); + expect(resolved.enable_signup).toBe(false); }); - expect(() => - run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.enabled", "auth.third_party.firebase.enabled"]), - ), - ), - ).not.toThrow(); }); - it("still rejects a malformed SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED when no remote block matched", () => { - testEnv["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED"] = "not-a-bool"; - const config = baseConfig({ - auth: { enabled: true, third_party: { firebase: { enabled: false } } }, - }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - 'Invalid config for auth.third_party.firebase.enabled: cannot parse "not-a-bool" as a bool', - ); - }); + describe("api.tls (cert/key validation)", () => { + const tempRoot = useTempWorkdir("supabase-api-tls-test-"); - it("suppresses a malformed SUPABASE_EDGE_RUNTIME_DENO_VERSION when a remote block already set edge_runtime.deno_version", () => { - testEnv["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = "abc"; - const config = baseConfig({ edge_runtime: { deno_version: 2 } }); - expect(() => - run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["edge_runtime.deno_version"]), - ), - ), - ).not.toThrow(); - }); + const writeTlsFile = (workdir: string, name: string, contents = "dummy") => + Effect.gen(function* () { + const supabaseDir = yield* join(workdir, "supabase"); + yield* mkdirp(supabaseDir); + yield* writeFile(yield* join(supabaseDir, name), contents); + }); - it("still rejects a malformed SUPABASE_EDGE_RUNTIME_DENO_VERSION when no remote block matched", () => { - testEnv["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = "abc"; - const config = baseConfig({ edge_runtime: { deno_version: 2 } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - "Failed reading config: Invalid edge_runtime.deno_version: abc.", + it.live("does not throw when tls.enabled with neither cert_path nor key_path set", () => + Effect.gen(function* () { + const config = baseConfig({ api: { tls: { enabled: true } } }); + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), ); - }); - it("suppresses a malformed SUPABASE_API_ENABLED when a remote block already set api.enabled", () => { - testEnv["SUPABASE_API_ENABLED"] = "not-a-bool"; - const config = baseConfig({ api: { enabled: false } }); - expect(() => - run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["api.enabled"]), - ), - ), - ).not.toThrow(); - }); + // The "exactly one of cert/key set" checks live in config-validate.unit.test.ts; the file-read + // behavior below is tested here. - it("still rejects a malformed SUPABASE_API_ENABLED when no remote block matched", () => { - testEnv["SUPABASE_API_ENABLED"] = "not-a-bool"; - const config = baseConfig({ api: { enabled: false } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - 'Invalid config for api.enabled: cannot parse "not-a-bool" as a bool', + it.live("throws an error when the configured cert file does not exist", () => + Effect.gen(function* () { + yield* writeTlsFile(tempRoot.current, "key.pem"); + const config = baseConfig({ + api: { tls: { enabled: true, cert_path: "missing-cert.pem", key_path: "key.pem" } }, + }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) + .message, + ).toContain("failed to read TLS cert: "); + }).pipe(Effect.provide(BunServices.layer)), ); - }); - - it("suppresses a malformed SUPABASE_STUDIO_ENABLED when a remote block already set studio.enabled", () => { - testEnv["SUPABASE_STUDIO_ENABLED"] = "not-a-bool"; - const config = baseConfig({ studio: { enabled: false } }); - expect(() => - run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["studio.enabled"]), - ), - ), - ).not.toThrow(); - }); - it("still rejects a malformed SUPABASE_STUDIO_ENABLED when no remote block matched", () => { - testEnv["SUPABASE_STUDIO_ENABLED"] = "not-a-bool"; - const config = baseConfig({ studio: { enabled: false } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - 'Invalid config for studio.enabled: cannot parse "not-a-bool" as a bool', + it.live("throws an error when the configured key file does not exist", () => + Effect.gen(function* () { + yield* writeTlsFile(tempRoot.current, "cert.pem"); + const config = baseConfig({ + api: { tls: { enabled: true, cert_path: "cert.pem", key_path: "missing-key.pem" } }, + }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) + .message, + ).toContain("failed to read TLS key: "); + }).pipe(Effect.provide(BunServices.layer)), ); - }); - - it("suppresses a malformed SUPABASE_STUDIO_PORT when a remote block already set studio.port", () => { - testEnv["SUPABASE_STUDIO_PORT"] = "not-a-port"; - const config = baseConfig({ studio: { port: 54323 } }); - expect(() => - run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["studio.port"]), - ), - ), - ).not.toThrow(); - }); - - it("suppresses a malformed SUPABASE_LOCAL_SMTP_ENABLED when a remote block already set local_smtp.enabled", () => { - testEnv["SUPABASE_LOCAL_SMTP_ENABLED"] = "not-a-bool"; - const config = baseConfig({ local_smtp: { enabled: false } }); - expect(() => - run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["local_smtp.enabled"]), - ), - ), - ).not.toThrow(); - }); - it("still rejects a malformed SUPABASE_LOCAL_SMTP_ENABLED when no remote block matched", () => { - testEnv["SUPABASE_LOCAL_SMTP_ENABLED"] = "not-a-bool"; - const config = baseConfig({ local_smtp: { enabled: false } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - 'Invalid config for local_smtp.enabled: cannot parse "not-a-bool" as a bool', + it.live("succeeds when both cert_path and key_path are readable", () => + Effect.gen(function* () { + yield* writeTlsFile(tempRoot.current, "cert.pem"); + yield* writeTlsFile(tempRoot.current, "key.pem"); + const config = baseConfig({ + api: { tls: { enabled: true, cert_path: "cert.pem", key_path: "key.pem" } }, + }); + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), ); - }); - it("suppresses a malformed SUPABASE_AUTH_ENABLE_SIGNUP when a remote block already set auth.enable_signup", () => { - testEnv["SUPABASE_AUTH_ENABLE_SIGNUP"] = "not-a-bool"; - const config = baseConfig({ auth: { enable_signup: false } }); - expect(() => - run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.enable_signup"]), - ), - ), - ).not.toThrow(); - }); - - it("still rejects a malformed SUPABASE_AUTH_ENABLE_SIGNUP when no remote block matched", () => { - testEnv["SUPABASE_AUTH_ENABLE_SIGNUP"] = "not-a-bool"; - const config = baseConfig({ auth: { enable_signup: false } }); - expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( - 'Invalid config for auth.enable_signup: cannot parse "not-a-bool" as a bool', + it.live( + "resolves cert_path/key_path against /supabase unconditionally, no isAbsolute guard", + () => + Effect.gen(function* () { + yield* writeTlsFile(tempRoot.current, "cert.pem"); + yield* writeTlsFile(tempRoot.current, "key.pem"); + const config = baseConfig({ + api: { + tls: { + enabled: true, + cert_path: "/cert.pem", + key_path: "/key.pem", + }, + }, + }); + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), ); - }); - - it("suppresses a malformed SUPABASE_AUTH_MINIMUM_PASSWORD_LENGTH when a remote block already set auth.minimum_password_length", () => { - testEnv["SUPABASE_AUTH_MINIMUM_PASSWORD_LENGTH"] = "not-a-number"; - const config = baseConfig({ auth: { minimum_password_length: 8 } }); - expect(() => - run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.minimum_password_length"]), - ), - ), - ).not.toThrow(); - }); - it("suppresses a malformed SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED when a remote block already set experimental.webhooks.enabled", () => { - testEnv["SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED"] = "not-a-bool"; - const config = baseConfig({ experimental: { webhooks: { enabled: true } } }); - expect(() => - run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["experimental.webhooks.enabled"]), - ), - ), - ).not.toThrow(); + it.live("skips TLS validation entirely when api is disabled", () => + Effect.gen(function* () { + const config = baseConfig({ + api: { enabled: false, tls: { enabled: true, cert_path: "missing-cert.pem" } }, + }); + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); }); +}); - it("suppresses a scheme-invalid SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI when a remote block already set that hook's uri", () => { - testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI"] = "ftp://example.com"; - const config = baseConfig({ - auth: { - hook: { - custom_access_token: { - enabled: true, - uri: "https://example.com/hook", - secrets: `v1,whsec_${"A".repeat(32)}`, - }, - }, - }, - }); - const document = { auth: { hook: { custom_access_token: { enabled: true } } } }; - expect(() => - run( - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - document, - new Set(["auth.hook.custom_access_token.uri"]), - ), - ), - ).not.toThrow(); +describe("narrowConfigEnum", () => { + it("returns a configured value that is one of the allowed values", () => { + expect(narrowConfigEnum("realtime.ip_version", "IPv6", ["IPv4", "IPv6"])).toBe("IPv6"); }); - it("still rejects a scheme-invalid SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI when no remote block matched that leaf", () => { - testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI"] = "ftp://example.com"; - const config = baseConfig({ - auth: { - hook: { - custom_access_token: { enabled: true, uri: "https://example.com/hook", secrets: "" }, - }, - }, - }); - const document = { auth: { hook: { custom_access_token: { enabled: true } } } }; - expect(() => - run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), - ).toThrow("auth.hook.custom_access_token.uri should be a HTTP, HTTPS, or pg-functions URI"); + it("fails with the config value error naming every allowed value", () => { + expect(() => narrowConfigEnum("realtime.ip_version", "v9", ["IPv4", "IPv6"])).toThrow( + new CliConfigValueError({ + path: "realtime.ip_version", + tier: "config", + message: 'Invalid config for realtime.ip_version: "v9" must be one of "IPv4", "IPv6"', + }), + ); }); }); @@ -4465,7 +1064,7 @@ describe("resolveLocalJwks", () => { { kty: "oct", k: Buffer.from("a".repeat(32)).toString("base64url") }, ], }); - }).pipe(Effect.provide(testLayer)), + }).pipe(Effect.provide(BunServices.layer)), ); it.live("publishes the public form of every signing key and omits the oct fallback", () => @@ -4486,7 +1085,7 @@ describe("resolveLocalJwks", () => { expect(parsed.keys[0]).not.toHaveProperty("d"); expect(parsed.keys[0]).not.toHaveProperty("p"); expect(parsed.keys.some((key) => key["kty"] === "oct")).toBe(false); - }).pipe(Effect.provide(testLayer)), + }).pipe(Effect.provide(BunServices.layer)), ); it.live("preserves a configured signing key's use/ext and filters key_ops to verify-only", () => @@ -4498,7 +1097,7 @@ describe("resolveLocalJwks", () => { const parsed = (yield* decodeJson(jwks)) as { keys: ReadonlyArray> }; expect(parsed.keys[0]).toMatchObject({ use: "sig", ext: true, key_ops: ["verify"] }); - }).pipe(Effect.provide(testLayer)), + }).pipe(Effect.provide(BunServices.layer)), ); it.live( @@ -4525,30 +1124,28 @@ describe("resolveLocalJwks", () => { }, ], }); - }).pipe(Effect.provide(testLayer)), + }).pipe(Effect.provide(BunServices.layer)), ); - it.live("throws a Go-worded error when the signing keys file does not exist", () => + it.live("throws an error when the signing keys file does not exist", () => Effect.gen(function* () { const config = baseConfig({ auth: { signing_keys_path: "missing.json" } }); expect( (yield* Effect.flip(runLocalJwks(config, tempRoot.current, "a".repeat(32)))).message, ).toContain("failed to read signing keys: "); - }).pipe(Effect.provide(testLayer)), + }).pipe(Effect.provide(BunServices.layer)), ); - it.live("throws a Go-worded error when the signing keys file is malformed JSON", () => + it.live("throws an error when the signing keys file is malformed JSON", () => Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const supabaseDir = path.join(tempRoot.current, "supabase"); - yield* fs.makeDirectory(supabaseDir, { recursive: true }); - yield* fs.writeFileString(path.join(supabaseDir, "signing_keys.json"), "not valid json"); + const supabaseDir = yield* join(tempRoot.current, "supabase"); + yield* mkdirp(supabaseDir); + yield* writeFile(yield* join(supabaseDir, "signing_keys.json"), "not valid json"); const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); expect( (yield* Effect.flip(runLocalJwks(config, tempRoot.current, "a".repeat(32)))).message, ).toContain("failed to decode signing keys: "); - }).pipe(Effect.provide(testLayer)), + }).pipe(Effect.provide(BunServices.layer)), ); describe("auth.third_party", () => { @@ -4564,7 +1161,7 @@ describe("resolveLocalJwks", () => { ).toContain( "Invalid config: auth.third_party.firebase is enabled but without a project_id.", ); - }).pipe(Effect.provide(testLayer)), + }).pipe(Effect.provide(BunServices.layer)), ); it.live("rejects more than one enabled third-party provider", () => @@ -4582,58 +1179,52 @@ describe("resolveLocalJwks", () => { ).toContain( "Invalid config: Only one third_party provider allowed to be enabled at a time.", ); - }).pipe(Effect.provide(testLayer)), + }).pipe(Effect.provide(BunServices.layer)), ); - it.live( - "does not validate third-party providers when auth is disabled, matching Go's ResolveJWKS/IssuerURL", - () => - Effect.gen(function* () { - const remoteKeys = [{ kty: "RSA", kid: "firebase-key", n: "abc", e: "AQAB" }]; - const issuerUrl = "https://securetoken.google.com/my-project"; - const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation((input) => { - const url = - typeof input === "string" - ? input - : input instanceof URL - ? input.toString() - : input.url; - if (url === `${issuerUrl}/.well-known/openid-configuration`) { - return Promise.resolve( - new Response(JSON.stringify({ jwks_uri: `${issuerUrl}/jwks.json` }), { - status: 200, - headers: { "content-type": "application/json" }, - }), - ); - } - if (url === `${issuerUrl}/jwks.json`) { - return Promise.resolve( - new Response(JSON.stringify({ keys: remoteKeys }), { - status: 200, - headers: { "content-type": "application/json" }, - }), - ); - } - return Promise.reject(new Error(`unexpected fetch: ${url}`)); - }); - const config = baseConfig({ - auth: { - enabled: false, - third_party: { - firebase: { enabled: true, project_id: "my-project" }, - workos: { enabled: true, issuer_url: "https://issuer.example" }, - }, + it.live("does not validate third-party providers when auth is disabled", () => + Effect.gen(function* () { + const remoteKeys = [{ kty: "RSA", kid: "firebase-key", n: "abc", e: "AQAB" }]; + const issuerUrl = "https://securetoken.google.com/my-project"; + const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation((input) => { + const url = + typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + if (url === `${issuerUrl}/.well-known/openid-configuration`) { + return Promise.resolve( + new Response(JSON.stringify({ jwks_uri: `${issuerUrl}/jwks.json` }), { + status: 200, + headers: { "content-type": "application/json" }, + }), + ); + } + if (url === `${issuerUrl}/jwks.json`) { + return Promise.resolve( + new Response(JSON.stringify({ keys: remoteKeys }), { + status: 200, + headers: { "content-type": "application/json" }, + }), + ); + } + return Promise.reject(new Error(`unexpected fetch: ${url}`)); + }); + const config = baseConfig({ + auth: { + enabled: false, + third_party: { + firebase: { enabled: true, project_id: "my-project" }, + workos: { enabled: true, issuer_url: "https://issuer.example" }, }, - }); - const jwksJson = yield* runLocalJwks(config, WORKDIR, "a".repeat(32)); - const jwks = (yield* decodeJson(jwksJson)) as { keys: ReadonlyArray<{ kid?: string }> }; - expect(jwks.keys.some((key) => key.kid === "firebase-key")).toBe(true); - fetchMock.mockRestore(); - }).pipe(Effect.provide(testLayer)), + }, + }); + const jwksJson = yield* runLocalJwks(config, WORKDIR, "a".repeat(32)); + const jwks = (yield* decodeJson(jwksJson)) as { keys: ReadonlyArray<{ kid?: string }> }; + expect(jwks.keys.some((key) => key.kid === "firebase-key")).toBe(true); + fetchMock.mockRestore(); + }).pipe(Effect.provide(BunServices.layer)), ); it.live( - 'does not attempt a remote JWKS fetch for an enabled third-party provider with an empty issuer_url, matching Go\'s issuerURL != "" check', + "does not attempt a remote JWKS fetch for an enabled third-party provider with an empty issuer_url", () => Effect.gen(function* () { const fetchMock = vi.spyOn(globalThis, "fetch"); @@ -4650,7 +1241,7 @@ describe("resolveLocalJwks", () => { expect(fetchMock).not.toHaveBeenCalled(); expect(jwks.keys.length).toBeGreaterThan(0); fetchMock.mockRestore(); - }).pipe(Effect.provide(testLayer)), + }).pipe(Effect.provide(BunServices.layer)), ); it.live("fetches and includes the remote JWKS for an enabled third-party provider", () => @@ -4692,7 +1283,7 @@ describe("resolveLocalJwks", () => { expect.arrayContaining([expect.objectContaining({ kid: "remote-key" })]), ); expect(fetchMock).toHaveBeenCalledTimes(2); - }).pipe(Effect.provide(testLayer)), + }).pipe(Effect.provide(BunServices.layer)), ); it.live( @@ -4711,216 +1302,7 @@ describe("resolveLocalJwks", () => { expect( (yield* Effect.flip(runLocalJwks(config, WORKDIR, "a".repeat(32)))).message, ).toContain("Failed to fetch https://issuer.example/.well-known/openid-configuration"); - }).pipe(Effect.provide(testLayer)), - ); - }); - - describe("remoteOverrideKeys (linked shadow provisioning, CLI-1956)", () => { - afterEach(() => { - for (const name of [ - "SUPABASE_AUTH_SIGNING_KEYS_PATH", - "SUPABASE_AUTH_THIRD_PARTY_WORKOS_ENABLED", - "SUPABASE_AUTH_THIRD_PARTY_WORKOS_ISSUER_URL", - "SUPABASE_AUTH_ENABLED", - ]) { - delete testEnv[name]; - } - }); - - it.live( - "prefers a remote-set auth.signing_keys_path over a conflicting SUPABASE_AUTH_SIGNING_KEYS_PATH", - () => - Effect.gen(function* () { - yield* writeSigningKeys(tempRoot.current, [generateRsaJwk()]); - testEnv["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "missing-file.json"; - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - const jwks = yield* runLocalJwks( - config, - tempRoot.current, - "a".repeat(32), - undefined, - new Set(["auth.signing_keys_path"]), - ); - const parsed = (yield* decodeJson(jwks)) as { - keys: ReadonlyArray>; - }; - expect(parsed.keys).toHaveLength(1); - expect(parsed.keys[0]).toMatchObject({ kty: "RSA", kid: "test-rsa-kid" }); - }).pipe(Effect.provide(testLayer)), - ); - - it.live( - "still rejects a missing SUPABASE_AUTH_SIGNING_KEYS_PATH override when no remote block matched", - () => - Effect.gen(function* () { - yield* writeSigningKeys(tempRoot.current, [generateRsaJwk()]); - testEnv["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "missing-file.json"; - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - expect( - (yield* Effect.flip(runLocalJwks(config, tempRoot.current, "a".repeat(32)))).message, - ).toContain("failed to read signing keys: "); - }).pipe(Effect.provide(testLayer)), - ); - - it.live("prefers a remote-set auth.third_party.workos.* over conflicting env overrides", () => - Effect.gen(function* () { - const remoteKeys = [{ kty: "RSA", kid: "remote-key", n: "abc", e: "AQAB" }]; - const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation((input) => { - const url = - typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - if (url === "https://remote-issuer.example/.well-known/openid-configuration") { - return Promise.resolve( - new Response( - JSON.stringify({ jwks_uri: "https://remote-issuer.example/jwks.json" }), - { status: 200, headers: { "content-type": "application/json" } }, - ), - ); - } - if (url === "https://remote-issuer.example/jwks.json") { - return Promise.resolve( - new Response(JSON.stringify({ keys: remoteKeys }), { - status: 200, - headers: { "content-type": "application/json" }, - }), - ); - } - return Promise.reject(new Error(`unexpected fetch url: ${url}`)); - }); - testEnv["SUPABASE_AUTH_THIRD_PARTY_WORKOS_ENABLED"] = "false"; - testEnv["SUPABASE_AUTH_THIRD_PARTY_WORKOS_ISSUER_URL"] = "https://env-should-not-win.test"; - const config = baseConfig({ - auth: { - third_party: { workos: { enabled: true, issuer_url: "https://remote-issuer.example" } }, - }, - }); - const jwks = yield* runLocalJwks( - config, - WORKDIR, - "a".repeat(32), - undefined, - new Set(["auth.third_party.workos.enabled", "auth.third_party.workos.issuer_url"]), - ); - const parsed = (yield* decodeJson(jwks)) as { - keys: ReadonlyArray>; - }; - expect(parsed.keys.some((key) => key["kid"] === "remote-key")).toBe(true); - fetchMock.mockRestore(); - }).pipe(Effect.provide(testLayer)), - ); - - it.live( - "suppresses a malformed SUPABASE_AUTH_ENABLED when a remote block already set auth.enabled", - () => - Effect.gen(function* () { - testEnv["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; - const config = baseConfig({ auth: { enabled: false } }); - expect( - yield* runLocalJwks( - config, - WORKDIR, - "a".repeat(32), - undefined, - new Set(["auth.enabled"]), - ), - ).toEqual(expect.any(String)); - }).pipe(Effect.provide(testLayer)), - ); - - it.live("still rejects a malformed SUPABASE_AUTH_ENABLED when no remote block matched", () => - Effect.gen(function* () { - testEnv["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; - const config = baseConfig({ auth: { enabled: false } }); - expect( - (yield* Effect.flip(runLocalJwks(config, WORKDIR, "a".repeat(32)))).message, - ).toContain('Invalid config for auth.enabled: cannot parse "not-a-bool" as a bool'); - }).pipe(Effect.provide(testLayer)), - ); - }); -}); - -describe("resolveAuthExternalUrl — remoteOverrideKeys (linked shadow provisioning, CLI-1956)", () => { - afterEach(() => { - delete testEnv["SUPABASE_AUTH_EXTERNAL_URL"]; - }); - - it("prefers a remote-set auth.external_url over a conflicting SUPABASE_AUTH_EXTERNAL_URL", () => { - testEnv["SUPABASE_AUTH_EXTERNAL_URL"] = "https://env-should-not-win.test"; - const document = { auth: { external_url: "https://remote.test" } }; - expect(run(resolveAuthExternalUrl(document, undefined, new Set(["auth.external_url"])))).toBe( - "https://remote.test", + }).pipe(Effect.provide(BunServices.layer)), ); }); - - it("still applies SUPABASE_AUTH_EXTERNAL_URL when no remote block matched", () => { - testEnv["SUPABASE_AUTH_EXTERNAL_URL"] = "https://env-wins.test"; - const document = { auth: { external_url: "https://configured.test" } }; - expect(run(resolveAuthExternalUrl(document, undefined))).toBe("https://env-wins.test"); - }); -}); - -describe("resolveConfiguredSigningKeys — remoteOverrideKeys (linked shadow provisioning, CLI-1956)", () => { - const tempRoot = useTempWorkdir("supabase-configured-signing-keys-test-"); - - afterEach(() => { - delete testEnv["SUPABASE_AUTH_SIGNING_KEYS_PATH"]; - delete testEnv["SUPABASE_AUTH_ENABLED"]; - }); - - it.live( - "prefers a remote-set auth.signing_keys_path over a conflicting SUPABASE_AUTH_SIGNING_KEYS_PATH", - () => - Effect.gen(function* () { - const jwk = generateRsaJwk(); - yield* writeSigningKeys(tempRoot.current, [jwk]); - testEnv["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "missing-file.json"; - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - const keys = yield* resolveConfiguredSigningKeys( - config, - tempRoot.current, - undefined, - new Set(["auth.signing_keys_path"]), - ); - expect(keys).toHaveLength(1); - expect(keys?.[0]).toMatchObject({ kid: "test-rsa-kid" }); - }).pipe(Effect.provide(testLayer)), - ); - - it.live("still reads the env-overridden path when no remote block matched", () => - Effect.gen(function* () { - const jwk = generateRsaJwk(); - yield* writeSigningKeys(tempRoot.current, [jwk]); - testEnv["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "missing-file.json"; - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - expect( - (yield* Effect.flip(resolveConfiguredSigningKeys(config, tempRoot.current, undefined))) - .message, - ).toContain("failed to read signing keys: "); - }).pipe(Effect.provide(testLayer)), - ); - - it.live( - "suppresses a malformed SUPABASE_AUTH_ENABLED when a remote block already set auth.enabled", - () => - Effect.gen(function* () { - testEnv["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; - const config = baseConfig({ auth: { enabled: false } }); - yield* resolveConfiguredSigningKeys( - config, - tempRoot.current, - undefined, - new Set(["auth.enabled"]), - ); - }).pipe(Effect.provide(testLayer)), - ); - - it.live("still rejects a malformed SUPABASE_AUTH_ENABLED when no remote block matched", () => - Effect.gen(function* () { - testEnv["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; - const config = baseConfig({ auth: { enabled: false } }); - expect( - (yield* Effect.flip(resolveConfiguredSigningKeys(config, tempRoot.current, undefined))) - .message, - ).toContain('Invalid config for auth.enabled: cannot parse "not-a-bool" as a bool'); - }).pipe(Effect.provide(testLayer)), - ); }); diff --git a/apps/cli/src/command-internal/go-jwt.ts b/apps/cli/src/command-internal/local-jwt.ts similarity index 83% rename from apps/cli/src/command-internal/go-jwt.ts rename to apps/cli/src/command-internal/local-jwt.ts index 926569386d..03569c25bd 100644 --- a/apps/cli/src/command-internal/go-jwt.ts +++ b/apps/cli/src/command-internal/local-jwt.ts @@ -1,6 +1,6 @@ import { createHmac, createPrivateKey, createSign } from "node:crypto"; import { DateTime } from "effect"; -import { encodeGoJsonCompact } from "./go-json.ts"; +import { encodeHtmlSafeJsonCompact } from "./html-safe-json.ts"; export { DEFAULT_SIGNING_KEY } from "@supabase/stack/defaults"; @@ -34,7 +34,7 @@ export interface Jwk { /** * HS256 signer for the default local-dev `anon`/`service_role` keys. {@link - * generateAsymmetricGoJwt} below covers the RS256/ES256 branch, taken when + * generateAsymmetricLocalJwt} below covers the RS256/ES256 branch, taken when * `auth.signing_keys_path` is configured. * * Does not reuse `@supabase/stack`'s `generateJwt`: that helper uses a different issuer, a @@ -43,17 +43,17 @@ export interface Jwk { * "now". */ -const GO_JWT_ISSUER = "supabase-demo"; -const GO_JWT_FIXED_EXP = 1983812996; +const LOCAL_JWT_ISSUER = "supabase-demo"; +const LOCAL_JWT_FIXED_EXP = 1983812996; function base64UrlEncode(input: string): string { return Buffer.from(input).toString("base64url"); } -export function generateGoJwt(secret: string, role: "anon" | "service_role"): string { +export function generateLocalJwt(secret: string, role: "anon" | "service_role"): string { const header = base64UrlEncode(JSON.stringify({ alg: "HS256", typ: "JWT" })); const payload = base64UrlEncode( - JSON.stringify({ iss: GO_JWT_ISSUER, role, exp: GO_JWT_FIXED_EXP }), + JSON.stringify({ iss: LOCAL_JWT_ISSUER, role, exp: LOCAL_JWT_FIXED_EXP }), ); const data = `${header}.${payload}`; const signature = createHmac("sha256", secret).update(data).digest("base64url"); @@ -61,7 +61,7 @@ export function generateGoJwt(secret: string, role: "anon" | "service_role"): st } /** Asymmetric-JWT expiry: 10 years from now. */ -const GO_JWT_ASYMMETRIC_EXPIRY_SECONDS = 60 * 60 * 24 * 365 * 10; +const LOCAL_JWT_ASYMMETRIC_EXPIRY_SECONDS = 60 * 60 * 24 * 365 * 10; function base64UrlToBigInt(value: string): bigint { const hex = Buffer.from(value, "base64url").toString("hex"); @@ -88,7 +88,7 @@ function modInverse(a: bigint, m: bigint): bigint { /** * Backfills the RSA CRT parameters (`dp`, `dq`, `qi`) when absent: Node's `createPrivateKey` - * rejects an RSA JWK without them, unlike Go, which derives them lazily from `p`/`q`/`d` before + * rejects an RSA JWK without them, so they are derived from `p`/`q`/`d` before * signing. Returns the key unchanged if all three are already present, or if `d`/`p`/`q` are * missing (an invalid key either way). */ @@ -141,13 +141,11 @@ function assertSupportedKty(jwk: Jwk): void { } /** - * Reproduces `encoding/base64`'s `CorruptInputError` message for each numeric field, since Go's - * unpadded base64 decoder rejects input Node's own JWK importer would silently accept (e.g. a - * `=`-padded coordinate) and sign a token Go could never have produced. + * Produces a `CorruptInputError` message for each numeric field, since unpadded base64 decoding + * rejects input Node's own JWK importer would silently accept (e.g. a `=`-padded coordinate). * - * Checks fields in Go's exact order (EC: x, y, d; RSA: n, e, d, p, q) so the first invalid field - * matches Go's first-failure-wins order. An absent field is skipped, matching Go's zero value - * decoding to zero bytes. + * Checks fields in a fixed order (EC: x, y, d; RSA: n, e, d, p, q) so the first invalid field + * is reported. An absent field is skipped and decodes to zero bytes. */ function assertDecodableJwkNumericFields(jwk: Jwk): void { const assertField = (label: string, value: string | undefined): void => { @@ -197,8 +195,8 @@ function assertKeyMatchesAlgorithm(jwk: Jwk, algorithm: SupportedJwtAlgorithm): * * `dsaEncoding: "ieee-p1363"` is required for ES256: Node's default ECDSA signature is * DER-encoded, not the raw (r‖s) format JWS requires. The header is serialized with - * {@link encodeGoJsonCompact}, not `JSON.stringify`, since a `kid` containing `<`/`>`/`&` must - * HTML-escape to sign the same bytes Go would. + * {@link encodeHtmlSafeJsonCompact}, not `JSON.stringify`, since a `kid` containing `<`/`>`/`&` must + * HTML-escape so the signed bytes are stable. */ export function signJwtWithJwk(jwk: Jwk, payloadJson: string): string { try { @@ -227,7 +225,7 @@ export function signJwtWithJwk(jwk: Jwk, payloadJson: string): string { jwk.kid !== undefined && jwk.kid.length > 0 ? { alg: algorithm, kid: jwk.kid, typ: "JWT" } : { alg: algorithm, typ: "JWT" }; - const headerEncoded = base64UrlEncode(encodeGoJsonCompact(header)); + const headerEncoded = base64UrlEncode(encodeHtmlSafeJsonCompact(header)); const payloadEncoded = base64UrlEncode(payloadJson); const data = `${headerEncoded}.${payloadEncoded}`; @@ -249,11 +247,11 @@ export function signJwtWithJwk(jwk: Jwk, payloadJson: string): string { /** * The RS256/ES256 signing path, used when `auth.signing_keys_path` resolves to a non-empty JWK * array — the first key in the file signs both the anon and service_role tokens. Same claim - * shape as {@link generateGoJwt} (`iss`/`role`/`exp`), except the expiry is 10 years from now. + * shape as {@link generateLocalJwt} (`iss`/`role`/`exp`), except the expiry is 10 years from now. */ -export function generateAsymmetricGoJwt(jwk: Jwk, role: "anon" | "service_role"): string { +export function generateAsymmetricLocalJwt(jwk: Jwk, role: "anon" | "service_role"): string { const expiresAt = Math.floor(DateTime.toEpochMillis(DateTime.nowUnsafe()) / 1000) + - GO_JWT_ASYMMETRIC_EXPIRY_SECONDS; - return signJwtWithJwk(jwk, JSON.stringify({ iss: GO_JWT_ISSUER, role, exp: expiresAt })); + LOCAL_JWT_ASYMMETRIC_EXPIRY_SECONDS; + return signJwtWithJwk(jwk, JSON.stringify({ iss: LOCAL_JWT_ISSUER, role, exp: expiresAt })); } diff --git a/apps/cli/src/command-internal/go-jwt.unit.test.ts b/apps/cli/src/command-internal/local-jwt.unit.test.ts similarity index 76% rename from apps/cli/src/command-internal/go-jwt.unit.test.ts rename to apps/cli/src/command-internal/local-jwt.unit.test.ts index 0fa779aa94..aeb0b635ac 100644 --- a/apps/cli/src/command-internal/go-jwt.unit.test.ts +++ b/apps/cli/src/command-internal/local-jwt.unit.test.ts @@ -5,11 +5,11 @@ import { describe, expect, it } from "vitest"; import { assertDecodableJwkAlgorithm, - generateAsymmetricGoJwt, - generateGoJwt, + generateAsymmetricLocalJwt, + generateLocalJwt, signJwtWithJwk, type Jwk, -} from "./go-jwt.ts"; +} from "./local-jwt.ts"; const SECRET = "super-secret-jwt-token-with-at-least-32-characters-long"; @@ -34,16 +34,16 @@ function decodeSegment(segment: string): string { return Buffer.from(segment, "base64url").toString("utf8"); } -describe("generateGoJwt", () => { - it("emits Go's exact JWT header (no extra fields, alg before typ)", () => { - const token = generateGoJwt(SECRET, "anon"); +describe("generateLocalJwt", () => { + it("emits the JWT header (no extra fields, alg before typ)", () => { + const token = generateLocalJwt(SECRET, "anon"); const [header] = token.split("."); expect(header).toBeDefined(); expect(decodeSegment(header ?? "")).toBe('{"alg":"HS256","typ":"JWT"}'); }); - it("emits the anon payload with Go's exact key order and fixed claims", () => { - const token = generateGoJwt(SECRET, "anon"); + it("emits the anon payload with the exact key order and fixed claims", () => { + const token = generateLocalJwt(SECRET, "anon"); const [, payload] = token.split("."); expect(payload).toBeDefined(); const raw = decodeSegment(payload ?? ""); @@ -56,15 +56,15 @@ describe("generateGoJwt", () => { expect(Object.keys(parsed)).not.toContain("is_anonymous"); }); - it("emits the service_role payload with Go's exact key order and fixed claims", () => { - const token = generateGoJwt(SECRET, "service_role"); + it("emits the service_role payload with the exact key order and fixed claims", () => { + const token = generateLocalJwt(SECRET, "service_role"); const [, payload] = token.split("."); const raw = decodeSegment(payload ?? ""); expect(raw).toBe('{"iss":"supabase-demo","role":"service_role","exp":1983812996}'); }); it("signs with plain HMAC-SHA256 over the base64url header.payload, base64url-encoded", () => { - const token = generateGoJwt(SECRET, "anon"); + const token = generateLocalJwt(SECRET, "anon"); const [header, payload, signature] = token.split("."); const expectedSignature = createHmac("sha256", SECRET) .update(`${header}.${payload}`) @@ -73,22 +73,22 @@ describe("generateGoJwt", () => { }); it("is deterministic across calls (no timestamp derived from Date.now())", () => { - const first = generateGoJwt(SECRET, "anon"); - const second = generateGoJwt(SECRET, "anon"); + const first = generateLocalJwt(SECRET, "anon"); + const second = generateLocalJwt(SECRET, "anon"); expect(first).toBe(second); }); it("produces different tokens for different secrets", () => { - const a = generateGoJwt(SECRET, "anon"); - const b = generateGoJwt("a-different-secret-value-1234567", "anon"); + const a = generateLocalJwt(SECRET, "anon"); + const b = generateLocalJwt("a-different-secret-value-1234567", "anon"); expect(a).not.toBe(b); }); }); -describe("generateAsymmetricGoJwt", () => { +describe("generateAsymmetricLocalJwt", () => { it("signs and verifies an RS256 token from an RSA JWK", () => { const jwk = generateRsaJwk("rsa-kid"); - const token = generateAsymmetricGoJwt(jwk, "anon"); + const token = generateAsymmetricLocalJwt(jwk, "anon"); return importJWK(publicJwkOf(jwk), "RS256") .then((publicKey) => jwtVerify(token, publicKey)) .then(({ payload, protectedHeader }) => { @@ -97,10 +97,10 @@ describe("generateAsymmetricGoJwt", () => { }); }); - it("signs an RS256 token from an RSA JWK missing CRT exponents (dp/dq/qi), matching Go", () => { + it("signs an RS256 token from an RSA JWK missing CRT exponents (dp/dq/qi)", () => { const jwk = generateRsaJwk("rsa-kid"); const { dp: _dp, dq: _dq, qi: _qi, ...jwkWithoutCrtParams } = jwk; - const token = generateAsymmetricGoJwt(jwkWithoutCrtParams, "anon"); + const token = generateAsymmetricLocalJwt(jwkWithoutCrtParams, "anon"); return importJWK(publicJwkOf(jwk), "RS256") .then((publicKey) => jwtVerify(token, publicKey)) .then(({ payload, protectedHeader }) => { @@ -111,7 +111,7 @@ describe("generateAsymmetricGoJwt", () => { it("signs and verifies an ES256 token from an EC JWK", () => { const jwk = generateEcJwk("ec-kid"); - const token = generateAsymmetricGoJwt(jwk, "service_role"); + const token = generateAsymmetricLocalJwt(jwk, "service_role"); return importJWK(publicJwkOf(jwk), "ES256") .then((publicKey) => jwtVerify(token, publicKey)) .then(({ payload, protectedHeader }) => { @@ -122,7 +122,7 @@ describe("generateAsymmetricGoJwt", () => { it("omits the kid header entirely when the JWK has no kid", () => { const jwk = generateRsaJwk(); - const token = generateAsymmetricGoJwt(jwk, "anon"); + const token = generateAsymmetricLocalJwt(jwk, "anon"); const [header] = token.split("."); const decoded = JSON.parse(Buffer.from(header ?? "", "base64url").toString()); expect(decoded).toEqual({ alg: "RS256", typ: "JWT" }); @@ -131,7 +131,7 @@ describe("generateAsymmetricGoJwt", () => { it("sets a ~10-year expiry computed from the current time, not a fixed timestamp", () => { const jwk = generateRsaJwk(); const before = Math.floor(DateTime.toEpochMillis(DateTime.nowUnsafe()) / 1000); - const token = generateAsymmetricGoJwt(jwk, "anon"); + const token = generateAsymmetricLocalJwt(jwk, "anon"); const [, payload] = token.split("."); const decoded = JSON.parse(Buffer.from(payload ?? "", "base64url").toString()); const tenYearsSeconds = 60 * 60 * 24 * 365 * 10; @@ -141,39 +141,41 @@ describe("generateAsymmetricGoJwt", () => { it("rejects an unsupported algorithm", () => { const jwk = { ...generateRsaJwk(), alg: "RS512" }; - expect(() => generateAsymmetricGoJwt(jwk, "anon")).toThrow("unsupported algorithm: RS512"); + expect(() => generateAsymmetricLocalJwt(jwk, "anon")).toThrow("unsupported algorithm: RS512"); }); it("rejects a JWK with no algorithm", () => { const { alg: _alg, ...jwkWithoutAlg } = generateRsaJwk(); - expect(() => generateAsymmetricGoJwt(jwkWithoutAlg, "anon")).toThrow("unsupported algorithm: "); + expect(() => generateAsymmetricLocalJwt(jwkWithoutAlg, "anon")).toThrow( + "unsupported algorithm: ", + ); }); it("rejects an EC key forged with alg: RS256 instead of signing garbage", () => { const jwk = { ...generateEcJwk(), alg: "RS256" }; - expect(() => generateAsymmetricGoJwt(jwk, "anon")).toThrow( + expect(() => generateAsymmetricLocalJwt(jwk, "anon")).toThrow( "failed to sign JWT: key is of invalid type: RSA sign expects *rsa.PrivateKey", ); }); it("rejects an RSA key forged with alg: ES256 instead of signing garbage", () => { const jwk = { ...generateRsaJwk(), alg: "ES256" }; - expect(() => generateAsymmetricGoJwt(jwk, "anon")).toThrow( + expect(() => generateAsymmetricLocalJwt(jwk, "anon")).toThrow( "failed to sign JWT: key is of invalid type: ECDSA sign expects *ecdsa.PrivateKey", ); }); - it("rejects an ES256 EC key whose curve is not P-256, wrapped like Go's GenerateAsymmetricJWT", () => { + it("rejects an ES256 EC key whose curve is not P-256, wrapped in the private-key conversion error", () => { const { privateKey } = generateKeyPairSync("ec", { namedCurve: "P-384" }); const jwk = { ...privateKey.export({ format: "jwk" }), kty: "EC", alg: "ES256" }; - expect(() => generateAsymmetricGoJwt(jwk, "anon")).toThrow( + expect(() => generateAsymmetricLocalJwt(jwk, "anon")).toThrow( "failed to convert JWK to private key: unsupported curve: P-384", ); }); - it("rejects a JWK with no kty at all, wrapped like Go's GenerateAsymmetricJWT", () => { + it("rejects a JWK with no kty at all, wrapped in the private-key conversion error", () => { const jwk = { kty: "oct" } as Jwk; - expect(() => generateAsymmetricGoJwt(jwk, "anon")).toThrow( + expect(() => generateAsymmetricLocalJwt(jwk, "anon")).toThrow( "failed to convert JWK to private key: unsupported key type: oct", ); }); @@ -181,15 +183,15 @@ describe("generateAsymmetricGoJwt", () => { it("rejects an ES256 EC key with no curve at all", () => { const jwk = generateEcJwk(); const { crv: _crv, ...jwkWithoutCurve } = jwk; - expect(() => generateAsymmetricGoJwt(jwkWithoutCurve, "anon")).toThrow( + expect(() => generateAsymmetricLocalJwt(jwkWithoutCurve, "anon")).toThrow( "failed to convert JWK to private key: unsupported curve: ", ); }); - it("rejects a padded EC coordinate instead of signing a token Go would refuse to produce (CLI-1961 Codex review finding)", () => { + it("rejects a padded EC coordinate instead of signing an invalid token", () => { const jwk = generateEcJwk("ec-kid"); const padded = { ...jwk, x: `${jwk.x}=` }; - expect(() => generateAsymmetricGoJwt(padded, "anon")).toThrow( + expect(() => generateAsymmetricLocalJwt(padded, "anon")).toThrow( /^failed to convert JWK to private key: failed to decode x coordinate: illegal base64 data at input byte \d+$/, ); }); @@ -197,21 +199,21 @@ describe("generateAsymmetricGoJwt", () => { it("rejects a padded RSA modulus the same way", () => { const jwk = generateRsaJwk("rsa-kid"); const padded = { ...jwk, n: `${jwk.n}=` }; - expect(() => generateAsymmetricGoJwt(padded, "anon")).toThrow( + expect(() => generateAsymmetricLocalJwt(padded, "anon")).toThrow( /^failed to convert JWK to private key: failed to decode modulus: illegal base64 data at input byte \d+$/, ); }); it("still signs successfully for unpadded (correctly-encoded) coordinates", () => { const jwk = generateEcJwk("ec-kid"); - expect(() => generateAsymmetricGoJwt(jwk, "anon")).not.toThrow(); + expect(() => generateAsymmetricLocalJwt(jwk, "anon")).not.toThrow(); }); }); describe("signJwtWithJwk", () => { it("signs the caller's exact pre-encoded payload string verbatim (no re-serialization)", () => { const jwk = generateEcJwk("ec-kid"); - // Unsorted and containing `&`, which Go's encoder would normally HTML-escape: this function + // Unsorted and containing `&`, which the encoder would normally HTML-escape: this function // must sign exactly the bytes it's given. const payloadJson = '{"role":"postgres","sb-role":"mgmt-api & co"}'; const token = signJwtWithJwk(jwk, payloadJson); @@ -225,7 +227,7 @@ describe("signJwtWithJwk", () => { }); }); - it("HTML-escapes the kid in the header like Go's json.Marshal, unlike JSON.stringify (CLI-1961 Codex review finding)", () => { + it("HTML-escapes the kid in the header, unlike JSON.stringify", () => { const jwk = generateEcJwk("ac&d"); const token = signJwtWithJwk(jwk, '{"role":"anon"}'); const [header] = token.split("."); @@ -245,7 +247,7 @@ describe("assertDecodableJwkAlgorithm", () => { expect(() => assertDecodableJwkAlgorithm(undefined)).not.toThrow(); }); - it("rejects an unsupported algorithm with Go's exact UnmarshalText message", () => { + it("rejects an unsupported algorithm with the allowed-values message", () => { expect(() => assertDecodableJwkAlgorithm("HS256")).toThrow("must be one of [RS256 ES256]"); }); }); diff --git a/apps/cli/src/command-internal/local-project-context.ts b/apps/cli/src/command-internal/local-project-context.ts index 6b910eeb3a..57ac81dba7 100644 --- a/apps/cli/src/command-internal/local-project-context.ts +++ b/apps/cli/src/command-internal/local-project-context.ts @@ -1,173 +1,58 @@ -import { - loadCliProjectEnvironment, - CliConfigSchema, - type LoadedCliConfig, - type CliConfig, -} from "@supabase/config/effect"; -import { loadCliConfig } from "@supabase/config/internal"; -import { - Config, - Console, - Crypto, - Effect, - FileSystem, - Option, - Path, - Predicate, - Schema, -} from "effect"; +import type { CliConfig } from "@supabase/config"; +import { Crypto, Effect, FileSystem, Option, Path } from "effect"; -import { recordOrioleDbTelemetry } from "./db-image.ts"; -import { resolveLocalProjectId, sanitizeProjectId } from "./docker-ids.ts"; -import { getHostname } from "./hostname.ts"; -import { - deprecatedOrioleDBVersionEnvWarning, - envOverride, - envOverrideMajorVersion, - envOverrideOrioleDBVersion, -} from "./local-config-values.ts"; -import { resolveProjectEnvironmentValues } from "./project-environment.ts"; -import { configParseCauseMessage } from "../shared/output/normalize-error.ts"; +import { CliConfigValues, type ResolvedCliConfig } from "../config/cli-config-values.service.ts"; import { RuntimeInfo } from "../shared/runtime/runtime-info.service.ts"; +import { + describeConfigLoadFailure, + loadLocalResolvedConfigContext, +} from "./resolved-config-context.ts"; +import { recordOrioleDbTelemetry } from "./db-image.ts"; -/** Config, resolved project env values, hostname, and sanitized project id for a command. */ +/** Effective config, project env file values, hostname, and sanitized project id for a command. */ export interface LocalProjectContext { + /** The decoded config with every override, default and normalizer applied. */ readonly config: CliConfig; - readonly projectEnvValues: Record; - /** `null` when no `supabase/config.toml` was found. */ - readonly loaded: LoadedCliConfig | null; + /** Values from `supabase/.env*` files only; a name the shell sets is never in here. */ + readonly projectEnvValues: Readonly>; + readonly resolvedConfig: ResolvedCliConfig; readonly hostname: string; - /** Sanitized project id; see {@link sanitizeProjectId}. */ + /** The project id sanitized for Docker resource names. */ readonly projectId: string; } export const loadLocalProjectContext = ( workdir: string, mapConfigLoadError: (message: string) => E, - // An already-resolved `--linked`/`--project-ref` value, when the caller has one; merges the - // matching `[remotes.]` block over the base config. Defaults to `undefined` (no remote - // merge) for callers that don't have one yet. + // An already-resolved `--linked`/`--project-ref` value, when the caller has one; it selects + // the matching `[remotes.]` block. `undefined` applies no remote. projectRef?: string, ): Effect.Effect< LocalProjectContext, E, - FileSystem.FileSystem | Path.Path | RuntimeInfo | Crypto.Crypto + FileSystem.FileSystem | Path.Path | RuntimeInfo | Crypto.Crypto | CliConfigValues > => Effect.gen(function* () { - const supabaseEnv = - Option.getOrUndefined( - yield* Config.option(Config.String("SUPABASE_ENV")).pipe( - Effect.mapError(() => - mapConfigLoadError("failed to resolve environment variable: SUPABASE_ENV"), - ), - ), - ) || "development"; - // `workdir` is already the fully-resolved chdir target, so `search: false` stops - // `@supabase/config` from climbing ancestors and picking up an unrelated project's - // config.toml when `workdir` has none of its own. - const projectEnv = yield* loadCliProjectEnvironment({ - cwd: workdir, - baseEnv: process.env, - search: false, - // Omits `.env.local` when `SUPABASE_ENV=test`, matching - // `resolveProjectEnvironmentValues`'s gating for the project-root pass. - skipEnvLocal: supabaseEnv === "test", - }).pipe( - Effect.mapError((cause) => mapConfigLoadError(`failed to read config: ${String(cause)}`)), - ); - - // Must resolve before `loadCliConfig` decodes config.toml: an `env(...)`-valued `project_id` - // needs these values available to the decoder already. `workdir` is passed through so dotenv - // files under `/supabase` are still discovered even when `projectEnv` is `null`. - const projectEnvValues = yield* resolveProjectEnvironmentValues( - projectEnv, - workdir, - supabaseEnv, - ).pipe( - Effect.mapError((cause) => mapConfigLoadError(`failed to read config: ${String(cause)}`)), - ); - - // An absent config.toml is not a failure — a project id still resolves from the workdir - // basename default. Only a malformed file is a hard error. - const loaded = yield* loadCliConfig(workdir, { - cliProjectEnv: projectEnv !== null ? { ...projectEnv, values: projectEnvValues } : undefined, - search: false, - // Restricts resolution to `supabase/config.toml`; without this, a workdir with a stray - // `config.json` would be preferred over it. - tomlOnly: true, - goViperCompat: true, - projectRef, - }).pipe( - Effect.mapError((cause) => - mapConfigLoadError( - `failed to read config: ${Predicate.isTagged(cause, "CliConfigParseError") ? configParseCauseMessage(cause.cause) : String(cause)}`, - ), - ), - ); - // Printed beside the loader's deprecated-config WARNs, on the same real console, so it shares - // their once-per-context-load cadence. - const orioleDBEnvWarning = yield* deprecatedOrioleDBVersionEnvWarning((name) => - envOverride(name, undefined, projectEnvValues), - ); - if (orioleDBEnvWarning !== undefined) { - yield* Console.error(orioleDBEnvWarning).pipe( - Effect.provideService(Console.Console, globalThis.console), + const { resolvedConfig, config, projectEnvValues, hostname, projectId } = + yield* loadLocalResolvedConfigContext(workdir, Option.fromNullishOr(projectRef)).pipe( + Effect.mapError((cause) => mapConfigLoadError(describeConfigLoadFailure(cause))), ); - } - const config = - loaded?.config ?? - (yield* Schema.decodeEffect(CliConfigSchema)({}).pipe( - Effect.mapError((cause) => mapConfigLoadError(`failed to read config: ${String(cause)}`)), - )); - const hostname = yield* getHostname().pipe( - Effect.mapError((cause) => - mapConfigLoadError(`failed to resolve hostname: ${cause.message}`), - ), - ); - const path = yield* Path.Path; - // When a `[remotes.]` block matched `projectRef` above, its own `project_id` field is - // what selected it, so a stale or differently-scoped `SUPABASE_PROJECT_ID` must not win over - // it here. - const projectId = sanitizeProjectId( - resolveLocalProjectId( - path, - loaded?.appliedRemote !== undefined - ? undefined - : (projectEnvValues["SUPABASE_PROJECT_ID"] ?? - Option.getOrUndefined( - yield* Config.option(Config.String("SUPABASE_PROJECT_ID")).pipe( - Effect.mapError(() => - mapConfigLoadError( - "failed to resolve environment variable: SUPABASE_PROJECT_ID", - ), - ), - ), - )), - config.project_id, - workdir, - projectRef, - ), - ); + const appliedRemote = Option.getOrUndefined(resolvedConfig.appliedRemote); yield* Effect.annotateCurrentSpan({ - "config.found": loaded !== null, - "config.remote_applied": loaded?.appliedRemote !== undefined, + "config.remote_applied": appliedRemote !== undefined, }); - return { config, projectEnvValues, loaded, hostname, projectId }; + return { + config, + projectEnvValues, + resolvedConfig, + hostname, + projectId, + }; }).pipe(Effect.withSpan("LocalProjectContext.load")); /** Records OrioleDB selection for commands whose only local config read is this context. */ export const recordLocalProjectOrioleDbTelemetry = (context: LocalProjectContext) => - Effect.all({ - version: envOverrideOrioleDBVersion( - context.config.db.orioledb_version, - context.projectEnvValues, - ), - majorVersion: envOverrideMajorVersion( - context.config.db.major_version, - context.projectEnvValues, - ), - }).pipe( - Effect.flatMap(({ version, majorVersion }) => recordOrioleDbTelemetry(version, majorVersion)), + recordOrioleDbTelemetry(context.config.db.orioledb_version, context.config.db.major_version).pipe( Effect.ignore, ); diff --git a/apps/cli/src/command-internal/local-project-context.unit.test.ts b/apps/cli/src/command-internal/local-project-context.unit.test.ts index 6491930d7c..1b121b4c5e 100644 --- a/apps/cli/src/command-internal/local-project-context.unit.test.ts +++ b/apps/cli/src/command-internal/local-project-context.unit.test.ts @@ -1,20 +1,14 @@ import { BunServices } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; -import { - Config, - ConfigProvider, - Data, - Effect, - Exit, - FileSystem, - Layer, - Option, - Path, -} from "effect"; +import { Config, ConfigProvider, Data, Effect, FileSystem, Layer, Option, Path } from "effect"; -import { useTempWorkdir, withConfigEnv, withEnvVar } from "../../tests/helpers/command-mocks.ts"; -import { loadLocalProjectContext } from "./local-project-context.ts"; +import { useTempWorkdir } from "../../tests/helpers/command-mocks.ts"; +import { mockOutput, processEnvLayer } from "../../tests/helpers/mocks.ts"; +import { CliConfigFlagInputs, makeCliConfigFlagInputs } from "../config/cli-config-flags.ts"; +import { cliConfigValuesLayer } from "../config/cli-config-values.layer.ts"; import { runtimeInfoLayer } from "../shared/runtime/runtime-info.layer.ts"; +import { sanitizeProjectId } from "../shared/config/project-id.ts"; +import { loadLocalProjectContext } from "./local-project-context.ts"; /** Stands in for the whole Docker-client env-key set, which a project dotenv file never reaches. */ const DOCKER_HOST_KEY = "DOCKER_HOST"; @@ -22,6 +16,8 @@ const DOCKER_HOST_KEY = "DOCKER_HOST"; /** Unlike Docker-client keys, this one is read at container-spawn time, so a project dotenv file can still set it. */ const BITBUCKET_CLONE_DIR_KEY = "BITBUCKET_CLONE_DIR"; +const REF = "abcdefghijklmnopqrst"; + class TestError extends Data.TaggedError("TestError")<{ readonly message: string }> {} const writeDotEnv = Effect.fnUntraced(function* (workdir: string, contents: string) { @@ -50,83 +46,89 @@ const processEnvValue = (name: string) => ), ); +/** Runs against a shell environment of exactly `env`, with the real config service. */ +const layerWithShellEnv = (env: Readonly> = {}) => + Layer.fresh( + Layer.mergeAll( + BunServices.layer, + runtimeInfoLayer, + cliConfigValuesLayer.pipe( + Layer.provide( + Layer.mergeAll( + BunServices.layer, + mockOutput().layer, + Layer.succeed(CliConfigFlagInputs, makeCliConfigFlagInputs()), + ), + ), + ), + processEnvLayer(env), + ), + ); + const tempRoot = useTempWorkdir("supabase-project-context-"); -describe("loadLocalProjectContext", () => { - it.effect( - "prefers a matched [remotes.]'s project_id over a conflicting SUPABASE_PROJECT_ID", - () => { - const ref = "abcdefghijklmnopqrst"; - const workdir = tempRoot.current; - return Effect.gen(function* () { - yield* writeConfigToml( - workdir, - ['project_id = "toml-project"', "[remotes.prod]", `project_id = "${ref}"`, ""].join("\n"), - ); +const loadContext = (workdir: string, projectRef?: string) => + loadLocalProjectContext(workdir, (message) => new TestError({ message }), projectRef); - const context = yield* loadLocalProjectContext( - workdir, - (message) => new TestError({ message }), - ref, - ); - expect(context.loaded?.appliedRemote).toBe("prod"); - expect(context.projectId).toBe(ref); - }).pipe( - (body) => withEnvVar("SUPABASE_PROJECT_ID", "local", body), - (body) => withConfigEnv({ SUPABASE_PROJECT_ID: "local" }, body), - Effect.provide(Layer.mergeAll(BunServices.layer, runtimeInfoLayer)), +describe("loadLocalProjectContext", () => { + it.effect("prefers SUPABASE_PROJECT_ID over a matched [remotes.]'s project_id", () => { + const workdir = tempRoot.current; + return Effect.gen(function* () { + yield* writeConfigToml( + workdir, + ['project_id = "toml-project"', "[remotes.prod]", `project_id = "${REF}"`, ""].join("\n"), ); - }, - ); - it.effect("still applies SUPABASE_PROJECT_ID when no [remotes.*] block matches the ref", () => { - const ref = "abcdefghijklmnopqrst"; + const context = yield* loadContext(workdir, REF); + expect(context.resolvedConfig.appliedRemote).toEqual(Option.some("prod")); + expect(context.projectId).toBe("local"); + }).pipe(Effect.provide(layerWithShellEnv({ SUPABASE_PROJECT_ID: "local" }))); + }); + + it.effect("names the project after the workdir, not the ref, when no project_id is set", () => { + const workdir = tempRoot.current; + return Effect.gen(function* () { + const path = yield* Path.Path; + yield* writeConfigToml(workdir, ""); + + const context = yield* loadContext(workdir, REF); + expect(context.resolvedConfig.appliedRemote).toEqual(Option.none()); + expect(context.projectId).toBe(sanitizeProjectId(path.basename(workdir))); + }).pipe(Effect.provide(layerWithShellEnv())); + }); + + it.effect("applies SUPABASE_PROJECT_ID when no [remotes.*] block matches the ref", () => { const workdir = tempRoot.current; return Effect.gen(function* () { yield* writeConfigToml(workdir, ['project_id = "toml-project"', ""].join("\n")); - const context = yield* loadLocalProjectContext( - workdir, - (message) => new TestError({ message }), - ref, - ); - expect(context.loaded?.appliedRemote).toBeUndefined(); + const context = yield* loadContext(workdir, REF); + expect(context.resolvedConfig.appliedRemote).toEqual(Option.none()); expect(context.projectId).toBe("env-project"); - }).pipe( - (body) => withEnvVar("SUPABASE_PROJECT_ID", "env-project", body), - (body) => withConfigEnv({ SUPABASE_PROJECT_ID: "env-project" }, body), - Effect.provide(Layer.mergeAll(BunServices.layer, runtimeInfoLayer)), - ); + }).pipe(Effect.provide(layerWithShellEnv({ SUPABASE_PROJECT_ID: "env-project" }))); }); - it.effect( - "does NOT install a project .env's DOCKER_HOST into process.env, matching Go's Docker client being frozen at binary startup, before godotenv.Load ever runs", - () => { - const workdir = tempRoot.current; - return Effect.gen(function* () { - yield* writeDotEnv(workdir, `DOCKER_HOST=tcp://project-dotenv-host:2375\n`); + it.effect("does not install a project .env's DOCKER_HOST into process.env", () => { + const workdir = tempRoot.current; + return Effect.gen(function* () { + yield* writeDotEnv(workdir, `DOCKER_HOST=tcp://project-dotenv-host:2375\n`); - yield* loadLocalProjectContext(workdir, (message) => new TestError({ message })); - expect(yield* processEnvValue(DOCKER_HOST_KEY)).toBeUndefined(); - }).pipe( - (body) => withEnvVar(DOCKER_HOST_KEY, undefined, body), - Effect.provide(Layer.mergeAll(BunServices.layer, runtimeInfoLayer)), - ); - }, - ); + yield* loadContext(workdir); + expect(yield* processEnvValue(DOCKER_HOST_KEY)).toBeUndefined(); + }).pipe(Effect.provide(layerWithShellEnv())); + }); it.effect( - "leaves an already-set shell DOCKER_HOST untouched regardless of a conflicting project .env value", + "leaves an already-set shell DOCKER_HOST untouched by a conflicting project .env", () => { const workdir = tempRoot.current; return Effect.gen(function* () { yield* writeDotEnv(workdir, `DOCKER_HOST=tcp://project-dotenv-host:2375\n`); - yield* loadLocalProjectContext(workdir, (message) => new TestError({ message })); + yield* loadContext(workdir); expect(yield* processEnvValue(DOCKER_HOST_KEY)).toBe("tcp://real-shell-host:2375"); }).pipe( - (body) => withEnvVar(DOCKER_HOST_KEY, "tcp://real-shell-host:2375", body), - Effect.provide(Layer.mergeAll(BunServices.layer, runtimeInfoLayer)), + Effect.provide(layerWithShellEnv({ [DOCKER_HOST_KEY]: "tcp://real-shell-host:2375" })), ); }, ); @@ -136,148 +138,41 @@ describe("loadLocalProjectContext", () => { return Effect.gen(function* () { yield* writeDotEnv(workdir, `BITBUCKET_CLONE_DIR=/opt/atlassian/pipelines/agent/build\n`); - const context = yield* loadLocalProjectContext( - workdir, - (message) => new TestError({ message }), - ); + const context = yield* loadContext(workdir); expect(context.projectEnvValues[BITBUCKET_CLONE_DIR_KEY]).toBe( "/opt/atlassian/pipelines/agent/build", ); expect(yield* processEnvValue(BITBUCKET_CLONE_DIR_KEY)).toBeUndefined(); - }).pipe( - (body) => withEnvVar(BITBUCKET_CLONE_DIR_KEY, undefined, body), - Effect.provide(Layer.mergeAll(BunServices.layer, runtimeInfoLayer)), - ); - }); - - it.effect("keeps the shell Bitbucket marker ahead of the project value", () => { - const workdir = tempRoot.current; - return Effect.gen(function* () { - yield* writeDotEnv(workdir, `BITBUCKET_CLONE_DIR=/opt/atlassian/pipelines/agent/build\n`); - - const context = yield* loadLocalProjectContext( - workdir, - (message) => new TestError({ message }), - ); - expect(context.projectEnvValues[BITBUCKET_CLONE_DIR_KEY]).toBe("/real-shell-clone-dir"); - expect(yield* processEnvValue(BITBUCKET_CLONE_DIR_KEY)).toBe("/real-shell-clone-dir"); - }).pipe( - (body) => withEnvVar(BITBUCKET_CLONE_DIR_KEY, "/real-shell-clone-dir", body), - Effect.provide(Layer.mergeAll(BunServices.layer, runtimeInfoLayer)), - ); - }); - - it.effect("falls back to config.toml's project_id when SUPABASE_PROJECT_ID is empty", () => { - const workdir = tempRoot.current; - return Effect.gen(function* () { - yield* writeConfigToml(workdir, ['project_id = "toml-project"', ""].join("\n")); - - const context = yield* loadLocalProjectContext( - workdir, - (message) => new TestError({ message }), - ); - expect(context.projectId).toBe("toml-project"); - }).pipe( - (body) => withEnvVar("SUPABASE_PROJECT_ID", "", body), - (body) => withConfigEnv({ SUPABASE_PROJECT_ID: "" }, body), - Effect.provide(Layer.mergeAll(BunServices.layer, runtimeInfoLayer)), - ); + }).pipe(Effect.provide(layerWithShellEnv())); }); it.effect( - "falls back to config.toml's project_id when only Config sees an empty SUPABASE_PROJECT_ID", + "leaves a shell-set key out of projectEnvValues so the shell value stays the source", () => { - // Windows env lookups are case-insensitive, so a lowercase `supabase_project_id=` reaches the - // Config read with "" while the ambient values only carry the lowercase key. const workdir = tempRoot.current; return Effect.gen(function* () { - yield* writeConfigToml(workdir, ['project_id = "toml-project"', ""].join("\n")); + yield* writeDotEnv(workdir, `BITBUCKET_CLONE_DIR=/opt/atlassian/pipelines/agent/build\n`); - const context = yield* loadLocalProjectContext( - workdir, - (message) => new TestError({ message }), - ); - expect(context.projectId).toBe("toml-project"); + const context = yield* loadContext(workdir); + expect(context.projectEnvValues[BITBUCKET_CLONE_DIR_KEY]).toBeUndefined(); + expect(yield* processEnvValue(BITBUCKET_CLONE_DIR_KEY)).toBe("/real-shell-clone-dir"); }).pipe( - (body) => withEnvVar("SUPABASE_PROJECT_ID", undefined, body), - (body) => withConfigEnv({ SUPABASE_PROJECT_ID: "" }, body), - Effect.provide(Layer.mergeAll(BunServices.layer, runtimeInfoLayer)), + Effect.provide(layerWithShellEnv({ [BITBUCKET_CLONE_DIR_KEY]: "/real-shell-clone-dir" })), ); }, ); - it.effect("loads the development dotenv files when SUPABASE_ENV is empty", () => { + it.effect("holds only project .env file values, never host environment variables", () => { const workdir = tempRoot.current; return Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - yield* writeConfigToml(workdir, ['project_id = "toml-project"', ""].join("\n")); - yield* fs.writeFileString( - path.join(workdir, "supabase", ".env.development"), - "SUPABASE_AUTH_JWT_SECRET=dev-secret\n", - ); + yield* writeConfigToml(workdir, ""); + yield* fs.writeFileString(path.join(workdir, "supabase", ".env"), "FROM_FILE=file-value\n"); - const context = yield* loadLocalProjectContext( - workdir, - (message) => new TestError({ message }), - ); - expect(context.projectEnvValues["SUPABASE_AUTH_JWT_SECRET"]).toBe("dev-secret"); - }).pipe( - (body) => withEnvVar("SUPABASE_AUTH_JWT_SECRET", undefined, body), - (body) => withConfigEnv({ SUPABASE_ENV: "" }, body), - Effect.provide(Layer.mergeAll(BunServices.layer, runtimeInfoLayer)), - ); - }); - - it.effect("skips an unparseable supabase/.env.local when SUPABASE_ENV is test", () => { - const workdir = tempRoot.current; - return Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - yield* writeConfigToml(workdir, ['project_id = "toml-project"', ""].join("\n")); - yield* fs.writeFileString(path.join(workdir, "supabase", ".env.local"), "not a valid line\n"); - - const exit = yield* loadLocalProjectContext( - workdir, - (message) => new TestError({ message }), - ).pipe(Effect.exit); - expect(Exit.isSuccess(exit)).toBe(true); - }).pipe( - (body) => withConfigEnv({ SUPABASE_ENV: "test" }, body), - Effect.provide(Layer.mergeAll(BunServices.layer, runtimeInfoLayer)), - ); - }); - - it.effect("fails on an unparseable supabase/.env.local outside the test environment", () => { - const workdir = tempRoot.current; - return Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - yield* writeConfigToml(workdir, ['project_id = "toml-project"', ""].join("\n")); - yield* fs.writeFileString(path.join(workdir, "supabase", ".env.local"), "not a valid line\n"); - - const error = yield* loadLocalProjectContext( - workdir, - (message) => new TestError({ message }), - ).pipe(Effect.flip); - expect(error.message).toMatch(/^failed to read config: /); - }).pipe( - (body) => withConfigEnv({ SUPABASE_ENV: "development" }, body), - Effect.provide(Layer.mergeAll(BunServices.layer, runtimeInfoLayer)), - ); - }); - - it.effect("names the failing field when supabase/config.toml fails to decode", () => { - const workdir = tempRoot.current; - return Effect.gen(function* () { - yield* writeConfigToml(workdir, '[db]\nport = "abc"\n'); - - const error = yield* loadLocalProjectContext( - workdir, - (message) => new TestError({ message }), - ).pipe(Effect.flip); - expect(error.message).toBe('failed to read config: Expected number\n at ["db"]["port"]'); - }).pipe(Effect.provide(Layer.mergeAll(BunServices.layer, runtimeInfoLayer))); + const context = yield* loadContext(workdir); + expect(context.projectEnvValues).toEqual({ FROM_FILE: "file-value" }); + }).pipe(Effect.provide(layerWithShellEnv({ FROM_SHELL: "shell-value" }))); }); it.effect("reports a TOML syntax error's position without quoting the file", () => { @@ -288,71 +183,10 @@ describe("loadLocalProjectContext", () => { '[edge_runtime.secrets]\nAPI_KEY = "sk_live_123"\nbroken =\n', ); - const error = yield* loadLocalProjectContext( - workdir, - (message) => new TestError({ message }), - ).pipe(Effect.flip); + const error = yield* loadContext(workdir).pipe(Effect.flip); expect(error.message).toBe( "failed to read config: Invalid TOML document: incomplete declaration: value expected (line 3, column 9)", ); - }).pipe(Effect.provide(Layer.mergeAll(BunServices.layer, runtimeInfoLayer))); - }); - - it.effect("keeps the Error: prefix when a project-root .env is malformed", () => { - const workdir = tempRoot.current; - return Effect.gen(function* () { - yield* writeDotEnv(workdir, "not a valid line\n"); - - const error = yield* loadLocalProjectContext( - workdir, - (message) => new TestError({ message }), - ).pipe(Effect.flip); - expect(error.message).toMatch( - /^failed to read config: Error: failed to parse environment file: /, - ); - }).pipe(Effect.provide(Layer.mergeAll(BunServices.layer, runtimeInfoLayer))); - }); - - it.effect("fails with a typed error when SUPABASE_ENV cannot be resolved", () => { - const workdir = tempRoot.current; - return Effect.gen(function* () { - const error = yield* loadLocalProjectContext( - workdir, - (message) => new TestError({ message }), - ).pipe(Effect.flip); - expect(error).toBeInstanceOf(TestError); - expect(error.message).toBe("failed to resolve environment variable: SUPABASE_ENV"); - }).pipe( - Effect.provideService( - ConfigProvider.ConfigProvider, - ConfigProvider.make(() => - Effect.fail(new ConfigProvider.SourceError({ message: "injected" })), - ), - ), - Effect.provide(Layer.mergeAll(BunServices.layer, runtimeInfoLayer)), - ); - }); - - it.effect("fails with a typed error when SUPABASE_PROJECT_ID cannot be resolved", () => { - const workdir = tempRoot.current; - return Effect.gen(function* () { - const error = yield* loadLocalProjectContext( - workdir, - (message) => new TestError({ message }), - ).pipe(Effect.flip); - expect(error).toBeInstanceOf(TestError); - expect(error.message).toBe("failed to resolve environment variable: SUPABASE_PROJECT_ID"); - }).pipe( - (body) => withEnvVar("SUPABASE_PROJECT_ID", undefined, body), - Effect.provideService( - ConfigProvider.ConfigProvider, - ConfigProvider.make((path) => - path[0] === "SUPABASE_PROJECT_ID" - ? Effect.fail(new ConfigProvider.SourceError({ message: "injected" })) - : ConfigProvider.fromEnvRecord({}).load(path), - ), - ), - Effect.provide(Layer.mergeAll(BunServices.layer, runtimeInfoLayer)), - ); + }).pipe(Effect.provide(layerWithShellEnv())); }); }); diff --git a/apps/cli/src/command-internal/local-stack-start-lock.ts b/apps/cli/src/command-internal/local-stack-start-lock.ts index d8af7a33a6..b6dd7a785b 100644 --- a/apps/cli/src/command-internal/local-stack-start-lock.ts +++ b/apps/cli/src/command-internal/local-stack-start-lock.ts @@ -8,8 +8,8 @@ import { type CliErrorActionabilityDeclaration, ErrorActionabilityId, } from "../shared/telemetry/error-actionability.ts"; +import { DebugLogger } from "../shared/output/debug-logger.service.ts"; import { aqua } from "./colors.ts"; -import { DebugLogger } from "./debug-logger.service.ts"; const SUBJECT = "the start lock"; const NO_WAIT = "0 millis"; diff --git a/apps/cli/src/command-internal/login-crypto.layer.ts b/apps/cli/src/command-internal/login-crypto.layer.ts index 75d6eece0a..54299e5f6f 100644 --- a/apps/cli/src/command-internal/login-crypto.layer.ts +++ b/apps/cli/src/command-internal/login-crypto.layer.ts @@ -27,7 +27,7 @@ export const loginCryptoLayer = Layer.sync(LoginCrypto, () => const host = hostname(); if (user && host) return `cli_${user}@${host}_${ts}`; } catch { - /* fall through to the fallback name (Go's generateTokenNameWithFallback) */ + /* fall through to the fallback name */ } return `cli_${ts}`; }), @@ -35,7 +35,7 @@ export const loginCryptoLayer = Layer.sync(LoginCrypto, () => Effect.try({ try: () => { const sharedSecret = ecdh.computeSecret(Buffer.from(payload.publicKey, "hex")); - // Go's `aesgcm.Open` expects the 16-byte GCM tag appended to the + // The payload carries the 16-byte GCM tag appended to the // ciphertext; Node wants it supplied separately via `setAuthTag`. const ciphertextHex = payload.ciphertext.slice(0, -32); const authTagHex = payload.ciphertext.slice(-32); diff --git a/apps/cli/src/command-internal/management-api-runtime.layer.ts b/apps/cli/src/command-internal/management-api-runtime.layer.ts index 0be3a1706d..419311f577 100644 --- a/apps/cli/src/command-internal/management-api-runtime.layer.ts +++ b/apps/cli/src/command-internal/management-api-runtime.layer.ts @@ -13,9 +13,9 @@ import { CommandPlatformApi } from "../auth/command-platform-api.service.ts"; import { commandPlatformApiLayer } from "../auth/command-platform-api.layer.ts"; import { CommandSettings } from "../config/command-settings.service.ts"; import { commandSettingsLayer } from "../config/command-settings.layer.ts"; -import { RemoteTarget } from "../config/remote-target.service.ts"; -import { remoteTargetLayer } from "../config/remote-target.layer.ts"; -import { DebugLogger } from "./debug-logger.service.ts"; +import { ProjectRefResolver } from "../config/project-ref.service.ts"; +import { projectRefLayer } from "../config/project-ref.layer.ts"; +import { DebugLogger } from "../shared/output/debug-logger.service.ts"; import { debugLoggerLayer } from "./debug-logger.layer.ts"; import { dohFetchLayer } from "./http-dns.ts"; import { IdentityStitch, identityStitchLayer } from "./identity-stitch.ts"; @@ -68,7 +68,7 @@ export function managementApiRuntimeLayer(subcommand: ReadonlyArray) { httpClient, credentials, cliSettings, - remoteTargetLayer.pipe(Layer.provide(platformApiFactory), Layer.provide(cliSettings)), + projectRefLayer.pipe(Layer.provide(platformApiFactory), Layer.provide(cliSettings)), linkedProjectCacheLayer.pipe( Layer.provide(credentials), Layer.provide(cliSettings), @@ -113,7 +113,7 @@ type ManagementApiServices = | HttpClient.HttpClient | CommandCredentials | CommandSettings - | RemoteTarget + | ProjectRefResolver | LinkedProjectCache | TelemetryState | CommandRuntime @@ -143,7 +143,7 @@ export function linkedDbResolverRuntimeLayer(subcommand: ReadonlyArray) httpClient, credentials, cliSettings, - remoteTargetLayer.pipe(Layer.provide(platformApiFactory), Layer.provide(cliSettings)), + projectRefLayer.pipe(Layer.provide(platformApiFactory), Layer.provide(cliSettings)), linkedProjectCacheLayer.pipe( Layer.provide(credentials), Layer.provide(cliSettings), @@ -155,22 +155,6 @@ export function linkedDbResolverRuntimeLayer(subcommand: ReadonlyArray) return built; } -/** Resolves only the hosted target, for commands that delegate to the Go binary. */ -export const remoteTargetRuntimeLayer = (() => { - const cliSettings = commandSettingsLayer.pipe(Layer.provide(debugLoggerLayer)); - const credentials = commandCredentialsLayer.pipe( - Layer.provide(cliSettings), - Layer.provide(debugLoggerLayer), - ); - const platformApiFactory = commandPlatformApiFactoryLayer.pipe( - Layer.provide(credentials), - Layer.provide(cliSettings), - Layer.provide(debugLoggerLayer), - Layer.provide(identityStitchLayer), - ); - return remoteTargetLayer.pipe(Layer.provide(platformApiFactory), Layer.provide(cliSettings)); -})(); - type LinkedDbResolverRuntime = ReturnType; export type LinkedDbResolverRuntimeRequirements = LinkedDbResolverRuntime extends Layer.Layer ? R : never; diff --git a/apps/cli/src/command-internal/migrate-and-seed.ts b/apps/cli/src/command-internal/migrate-and-seed.ts index bae901144d..1bf8470264 100644 --- a/apps/cli/src/command-internal/migrate-and-seed.ts +++ b/apps/cli/src/command-internal/migrate-and-seed.ts @@ -45,6 +45,22 @@ const migrationApplyError = ( }); }; +/** Warns a versionless `--experimental` rebuild that `schema_paths` is ignored while pg-delta is on. */ +export const experimentalSchemaPathsIgnoredWarning = + "WARNING: [db.migrations].schema_paths is not applied while pg-delta is enabled (the default). Set [experimental.pgdelta] enabled = false to apply schema_paths files.\n"; + +/** Whether {@link experimentalSchemaPathsIgnoredWarning} applies to this rebuild. */ +export const ignoresExperimentalSchemaPaths = (inputs: { + readonly experimental: boolean; + readonly version: string; + readonly pgDeltaEnabled: boolean; + readonly schemaPaths: ReadonlyArray; +}): boolean => + inputs.experimental && + inputs.version.length === 0 && + inputs.pgDeltaEnabled && + inputs.schemaPaths.length > 0; + /** * Reapplies local migrations up to `version`, then runs seed files. * @@ -64,6 +80,9 @@ export const migrateAndSeed = ( ) => Effect.gen(function* () { const output = yield* Output; + if (ignoresExperimentalSchemaPaths({ ...config, version })) { + yield* output.raw(experimentalSchemaPathsIgnoredWarning, "stderr"); + } if (config.experimental && version.length === 0 && !config.pgDeltaEnabled) { yield* applySchemaFiles( session, diff --git a/apps/cli/src/command-internal/migrate-and-seed.unit.test.ts b/apps/cli/src/command-internal/migrate-and-seed.unit.test.ts index 77a9d07a61..705f596254 100644 --- a/apps/cli/src/command-internal/migrate-and-seed.unit.test.ts +++ b/apps/cli/src/command-internal/migrate-and-seed.unit.test.ts @@ -144,12 +144,14 @@ layer(BunServices.layer)("migrateAndSeed experimental declarative-schema branch" ); expect(execs).toContain("create table schema_marker ()"); expect(execs).not.toContain("create table migration_marker ()"); - expect(out.rawChunks.map((c) => c.text).join("")).not.toContain("Applying migration"); + const stderr = out.rawChunks.map((c) => c.text).join(""); + expect(stderr).not.toContain("Applying migration"); + expect(stderr).not.toContain("schema_paths is not applied"); }), ); it.effect( - "falls back to migration files when pg-delta is enabled, even with experimental on and an empty version", + "falls back to migration files and warns that schema_paths is ignored when pg-delta is enabled, even with experimental on and an empty version", () => Effect.gen(function* () { const workdir = yield* makeWorkdir; @@ -175,6 +177,9 @@ layer(BunServices.layer)("migrateAndSeed experimental declarative-schema branch" ); expect(execs).toContain("create table migration_marker ()"); expect(execs).not.toContain("create table schema_marker ()"); + expect(out.rawChunks.map((c) => c.text).join("")).toContain( + "[db.migrations].schema_paths is not applied while pg-delta is enabled", + ); }), ); @@ -236,6 +241,40 @@ layer(BunServices.layer)("migrateAndSeed experimental declarative-schema branch" }), ); + it.effect( + "warns that schema_paths is ignored even when migrations are disabled and pg-delta is enabled", + () => + Effect.gen(function* () { + const workdir = yield* makeWorkdir; + yield* writeFile(workdir, "supabase/schemas/a.sql", "create table schema_marker ();"); + yield* writeFile( + workdir, + "supabase/migrations/20240101000000_x.sql", + "create table migration_marker ();", + ); + const { session, execs } = fakeSession(); + const out = mockOutput(); + yield* run( + workdir, + "", + { + ...baseConfig, + migrationsEnabled: false, + experimental: true, + pgDeltaEnabled: true, + schemaPaths: ["supabase/schemas/a.sql"], + }, + session, + out, + ); + expect(execs).not.toContain("create table migration_marker ()"); + expect(execs).not.toContain("create table schema_marker ()"); + expect(out.rawChunks.map((c) => c.text).join("")).toContain( + "[db.migrations].schema_paths is not applied while pg-delta is enabled", + ); + }), + ); + it.effect("still seeds after the declarative-schema branch runs", () => Effect.gen(function* () { const workdir = yield* makeWorkdir; @@ -384,7 +423,7 @@ layer(BunServices.layer)("migrateAndSeed experimental declarative-schema branch" }), ); - it.effect("attaches the failing schema file as Go's CmdSuggestion (See schema file: )", () => + it.effect("attaches the failing schema file as a suggestion (See schema file: )", () => Effect.gen(function* () { const workdir = yield* makeWorkdir; const schemaPath = "supabase/schemas/broken.sql"; diff --git a/apps/cli/src/command-internal/migration-apply.ts b/apps/cli/src/command-internal/migration-apply.ts index ccc8d33b4f..40eb63121c 100644 --- a/apps/cli/src/command-internal/migration-apply.ts +++ b/apps/cli/src/command-internal/migration-apply.ts @@ -1,5 +1,6 @@ -import { Config, Data, Effect, Option, type FileSystem, type Path } from "effect"; +import { Data, Effect, type FileSystem, type Path } from "effect"; +import { envValue } from "../shared/config/env-option.ts"; import { Output } from "../shared/output/output.service.ts"; import { bold } from "./colors.ts"; import { DbConnectError, DbExecError } from "./db-connection.errors.ts"; @@ -18,7 +19,7 @@ import { } from "./migration-history.ts"; import { parseMigrationContent } from "./migration-file.ts"; import { sqlFilesGlob } from "./sql-files-glob.ts"; -import { splitSqlTokens } from "./sql-split.ts"; +import { splitSqlTokens, trimLeadingSqlComments } from "./sql-split.ts"; /** * A migration file failed to apply. Used by `migration up`/`down`'s migrate-and-seed step; the @@ -39,9 +40,6 @@ export class MigrationApplyError extends Data.TaggedError("MigrationApplyError") } } -// Byte order mark (U+FEFF), stripped from the head of a statement. -const BOM_CODE_POINT = 0xfeff; - // Statements that PostgreSQL refuses to run inside a transaction block / extended-query // pipeline (SQLSTATE 25001). Matched against the upper-cased, comment-stripped statement. // @@ -57,31 +55,6 @@ const CLUSTER_PATTERN = /^CLUSTER(?:\s|$)/u; const TRANSACTION_CONTROL_PATTERN = /^(?:BEGIN|START\s+TRANSACTION|COMMIT|END|ABORT|PREPARE\s+TRANSACTION)(?:\s|$)/u; -/** - * Strips a leading BOM, whitespace, and SQL line (`--`) and block comments from the - * front of a statement so the keyword check below sees the real first token. - */ -const trimLeadingSqlComments = (sql: string): string => { - // Stripped via code point comparison so it isn't relied on to match a whitespace regex class. - let trimmed = sql.replace(/^[ \t\n\r]+/u, ""); - while (trimmed.charCodeAt(0) === BOM_CODE_POINT) { - trimmed = trimmed.slice(1).replace(/^[ \t\n\r]+/u, ""); - } - for (;;) { - if (trimmed.startsWith("--")) { - const idx = trimmed.indexOf("\n"); - if (idx < 0) return ""; - trimmed = trimmed.slice(idx + 1).replace(/^[ \t\n\r]+/u, ""); - } else if (trimmed.startsWith("/*")) { - const idx = trimmed.indexOf("*/"); - if (idx < 0) return trimmed; - trimmed = trimmed.slice(idx + 2).replace(/^[ \t\n\r]+/u, ""); - } else { - return trimmed.trim(); - } - } -}; - /** * Whether a migration statement cannot run inside a transaction block — `CREATE [UNIQUE] INDEX * CONCURRENTLY`, `DROP INDEX CONCURRENTLY`, `REINDEX … CONCURRENTLY`, `VACUUM`, `ALTER SYSTEM`, @@ -143,14 +116,14 @@ const utf8ByteLength = (value: string): number => new TextEncoder().encode(value // The scanner buffer starts at this size before applying the configured/default max, so a // statement must reach at least this many bytes before an oversized-token error can fire, // regardless of how small an override is set. -const GO_SCANNER_START_BUF_SIZE = 4096; +const SCANNER_START_BUF_SIZE = 4096; // Fallback cap when `SUPABASE_SCANNER_BUFFER_SIZE` is set but parses to a non-positive size, // including a value that can't be parsed at all (e.g. a bare "5M" with no trailing "B"). -const GO_DEFAULT_MAX_SCANNER_CAPACITY = 256 * 1024; +const DEFAULT_MAX_SCANNER_CAPACITY = 256 * 1024; -const GO_MAX_INT64 = 9223372036854775807n; -const GO_MIN_INT64 = -9223372036854775808n; +const INT64_MAX = 9223372036854775807n; +const INT64_MIN = -9223372036854775808n; /** * Parses a base-0 integer literal: decimal, or `0x`/`0o`/`0b`-prefixed hex/octal/binary, or a @@ -158,7 +131,7 @@ const GO_MIN_INT64 = -9223372036854775808n; * the same base (never leading, trailing, or doubled). Returns `undefined` for anything invalid * or outside the 64-bit signed integer range. */ -const parseGoBaseZeroInt = (value: string): number | undefined => { +const parseBaseZeroInt = (value: string): number | undefined => { const negative = value.startsWith("-"); const unsigned = negative || value.startsWith("+") ? value.slice(1) : value; if (unsigned.length === 0) return undefined; @@ -198,7 +171,7 @@ const parseGoBaseZeroInt = (value: string): number | undefined => { const bigPrefix = base === 16 ? "0x" : base === 8 ? "0o" : base === 2 ? "0b" : ""; const magnitude = BigInt(`${bigPrefix}${cleanDigits}`); const signedMagnitude = negative ? -magnitude : magnitude; - if (signedMagnitude > GO_MAX_INT64 || signedMagnitude < GO_MIN_INT64) return undefined; + if (signedMagnitude > INT64_MAX || signedMagnitude < INT64_MIN) return undefined; const n = Number.parseInt(cleanDigits, base); return negative ? -n : n; @@ -206,8 +179,8 @@ const parseGoBaseZeroInt = (value: string): number | undefined => { // Drops a decimal literal's fractional part rather than rounding (`"5.5"` → `"5"`); a // `0x`/`0o`/`0b` literal (which contains letters) never matches and passes through unchanged for -// {@link parseGoBaseZeroInt} to accept or reject. -const trimGoDecimal = (value: string): string => { +// {@link parseBaseZeroInt} to accept or reject. +const trimDecimalFraction = (value: string): string => { if (!value.includes(".")) return value; const match = /^([+-]?\d*)(?:\.\d*)?$/.exec(value); if (!match) return value; @@ -220,7 +193,7 @@ const trimGoDecimal = (value: string): string => { * `SUPABASE_SCANNER_BUFFER_SIZE` accepts an integer byte count, optionally suffixed * `k`/`m`/`g` (× 1024/1024²/1024³) immediately followed by a trailing `b`/`B` (e.g. `"5MB"`). A * bare `"5M"` (no trailing `B`) is NOT 5 MiB — it fails to parse as an integer and is treated as - * unset (`0`), same as any other unparseable or non-positive value. See {@link parseGoBaseZeroInt} + * unset (`0`), same as any other unparseable or non-positive value. See {@link parseBaseZeroInt} * for the accepted integer-literal grammar (hex/octal/binary prefixes, `_` separators). */ const parseScannerBufferSize = (raw: string): number => { @@ -246,7 +219,7 @@ const parseScannerBufferSize = (raw: string): number => { break; } } - const size = parseGoBaseZeroInt(trimGoDecimal(value)); + const size = parseBaseZeroInt(trimDecimalFraction(value)); return size !== undefined && Number.isFinite(size) && size > 0 ? size * multiplier : 0; }; @@ -265,31 +238,27 @@ export const checkScannerBufferSize = ( projectEnv: Readonly> = {}, ): Effect.Effect => Effect.gen(function* () { - const shellValue = yield* Config.option(Config.String("SUPABASE_SCANNER_BUFFER_SIZE")).pipe( - Effect.mapError(() => - mapError("failed to resolve environment variable: SUPABASE_SCANNER_BUFFER_SIZE", "read"), - ), - ); - const raw = Option.getOrUndefined(shellValue) ?? projectEnv["SUPABASE_SCANNER_BUFFER_SIZE"]; + const shell = yield* envValue("SUPABASE_SCANNER_BUFFER_SIZE"); + const raw = shell ?? projectEnv["SUPABASE_SCANNER_BUFFER_SIZE"]; if (raw === undefined) return; const configuredLimit = parseScannerBufferSize(raw); // Covers both an explicit non-positive size and an unparseable value (see - // `GO_DEFAULT_MAX_SCANNER_CAPACITY` above) — both fall back to the hardcoded default cap, not + // `DEFAULT_MAX_SCANNER_CAPACITY` above) — both fall back to the hardcoded default cap, not // to "no limit". const limit = configuredLimit > 0 - ? Math.max(configuredLimit, GO_SCANNER_START_BUF_SIZE) - : GO_DEFAULT_MAX_SCANNER_CAPACITY; - // The reported limit is the raw configured value, even below the `GO_SCANNER_START_BUF_SIZE` + ? Math.max(configuredLimit, SCANNER_START_BUF_SIZE) + : DEFAULT_MAX_SCANNER_CAPACITY; + // The reported limit is the raw configured value, even below the `SCANNER_START_BUF_SIZE` // floor (which only affects when the too-long error can fire, not the number reported), or the // hardcoded default once that's been fallen back to. - const reportedLimit = configuredLimit > 0 ? configuredLimit : GO_DEFAULT_MAX_SCANNER_CAPACITY; + const reportedLimit = configuredLimit > 0 ? configuredLimit : DEFAULT_MAX_SCANNER_CAPACITY; let emitted = 0; let lastRaw = ""; for (const token of splitSqlTokens(content)) { // A terminated token exactly at `limit` still succeeds (only strictly-over fails, `>`); an - // unterminated trailing token at `limit` already fails (`>=`), since there's no delimiter - // left to find once the buffer fills without one. + // unterminated trailing token at `limit` already fails (`>=`), since there's no delimiter left + // to find once the buffer fills without one. const tooLong = token.terminated ? utf8ByteLength(token.raw) > limit : utf8ByteLength(token.raw) >= limit; @@ -297,14 +266,14 @@ export const checkScannerBufferSize = ( const suggestion = `Try setting SUPABASE_SCANNER_BUFFER_SIZE=5MB (current size is ${Math.floor(reportedLimit / 1024)}KB)`; return yield* Effect.fail( mapError( - `bufio.Scanner: token too long\nAfter statement ${emitted}: ${lastRaw}\n${suggestion}`, + `scanner: token too long\nAfter statement ${emitted}: ${lastRaw}\n${suggestion}`, "read", ), ); } // `lastRaw` updates on every scanned token, even ones that trim to empty and don't advance - // `emitted` — so a lone `;` right before an oversized statement reports it accurately - // instead of a blank token. + // `emitted` — so a lone `;` right before an oversized statement reports it accurately instead + // of a blank token. lastRaw = token.raw; if (token.trimmed.length > 0) { emitted += 1; diff --git a/apps/cli/src/command-internal/migration-apply.unit.test.ts b/apps/cli/src/command-internal/migration-apply.unit.test.ts index ad6062ab34..224e8f2618 100644 --- a/apps/cli/src/command-internal/migration-apply.unit.test.ts +++ b/apps/cli/src/command-internal/migration-apply.unit.test.ts @@ -256,18 +256,16 @@ layer(BunServices.layer)("applyMigrationFile", (it) => { }), ); - it.effect( - "wraps a read failure with Go's parse-file error text (Go NewMigrationFromFile parity)", - () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const dir = yield* fs.makeTempDirectoryScoped({ prefix: "apply-read-fail-" }); - const missingFile = path.join(dir, "20240101120000_missing.sql"); - const { session } = fakeSession(); - const error = yield* run(session, missingFile).pipe(Effect.flip); - expect(error.message).toContain("failed to open migration file: "); - }), + it.effect("wraps a read failure with the parse-file error text", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = yield* fs.makeTempDirectoryScoped({ prefix: "apply-read-fail-" }); + const missingFile = path.join(dir, "20240101120000_missing.sql"); + const { session } = fakeSession(); + const error = yield* run(session, missingFile).pipe(Effect.flip); + expect(error.message).toContain("failed to open migration file: "); + }), ); it.effect("runs a pipeline-incompatible statement outside the surrounding transaction", () => @@ -713,7 +711,7 @@ describe("hasTransactionControl", () => { }); }); -layer(BunServices.layer)("migration failure rendering (Go ExecBatch parity)", (it) => { +layer(BunServices.layer)("migration failure rendering", (it) => { // Error message layout: `\n[Detail]\n[42704 hint]\n` then // `At statement: \n`. const failing = ( @@ -994,7 +992,7 @@ layer(BunServices.layer)("applySchemaFiles", (it) => { }); it.effect( - "reports a read failure with the workdir-relative path, not the absolute path used to read it (Go open supabase/... parity)", + "reports a read failure with the workdir-relative path, not the absolute path used to read it", () => Effect.gen(function* () { // An unreadable file (permission denied, not missing) still passes the glob's own @@ -1019,27 +1017,25 @@ layer(BunServices.layer)("applySchemaFiles", (it) => { }), ); - it.effect( - "rejects an oversized statement when SUPABASE_SCANNER_BUFFER_SIZE is configured (Go bufio.Scanner: token too long parity)", - () => - Effect.gen(function* () { - // Exceeds the 4096-byte floor the scanner always starts at, regardless of the configured - // limit. - const { workdir } = yield* schemaFile( - "schema-files-scanner-", - "big.sql", - `SELECT 1;\nSELECT '${"a".repeat(5000)}';\n`, - ); - const { session } = fakeSession(); - const error = yield* applySchema(session, workdir, "supabase/big.sql").pipe(Effect.flip); - expect(error.message).toContain("bufio.Scanner: token too long"); - expect(error.message).toContain("After statement 1: SELECT 1;"); - expect(error.message).toContain("Try setting SUPABASE_SCANNER_BUFFER_SIZE=5MB"); - }).pipe((body) => withConfigEnv({ SUPABASE_SCANNER_BUFFER_SIZE: "100b" }, body)), + it.effect("rejects an oversized statement when SUPABASE_SCANNER_BUFFER_SIZE is configured", () => + Effect.gen(function* () { + // Exceeds the 4096-byte floor the scanner always starts at, regardless of the configured + // limit. + const { workdir } = yield* schemaFile( + "schema-files-scanner-", + "big.sql", + `SELECT 1;\nSELECT '${"a".repeat(5000)}';\n`, + ); + const { session } = fakeSession(); + const error = yield* applySchema(session, workdir, "supabase/big.sql").pipe(Effect.flip); + expect(error.message).toContain("scanner: token too long"); + expect(error.message).toContain("After statement 1: SELECT 1;"); + expect(error.message).toContain("Try setting SUPABASE_SCANNER_BUFFER_SIZE=5MB"); + }).pipe((body) => withConfigEnv({ SUPABASE_SCANNER_BUFFER_SIZE: "100b" }, body)), ); it.effect( - "reports the last scanned RAW token in the too-long error even when it trimmed to empty (Go scanner.Text() parity, review CLI-1958)", + "reports the last scanned RAW token in the too-long error even when it trimmed to empty", () => Effect.gen(function* () { const { workdir } = yield* schemaFile( @@ -1049,13 +1045,13 @@ layer(BunServices.layer)("applySchemaFiles", (it) => { ); const { session } = fakeSession(); const error = yield* applySchema(session, workdir, "supabase/big.sql").pipe(Effect.flip); - expect(error.message).toContain("bufio.Scanner: token too long"); + expect(error.message).toContain("scanner: token too long"); expect(error.message).toContain("After statement 0: ;"); }).pipe((body) => withConfigEnv({ SUPABASE_SCANNER_BUFFER_SIZE: "100b" }, body)), ); it.effect( - "applies an oversized statement fine when SUPABASE_SCANNER_BUFFER_SIZE is unset (Go's default auto-grows to file size)", + "applies an oversized statement fine when SUPABASE_SCANNER_BUFFER_SIZE is unset (the buffer auto-grows to file size)", () => Effect.gen(function* () { const { workdir } = yield* schemaFile( @@ -1070,7 +1066,7 @@ layer(BunServices.layer)("applySchemaFiles", (it) => { ); it.effect( - "falls back to Go's hardcoded default cap when SUPABASE_SCANNER_BUFFER_SIZE is set but unparseable (viper parity, not '5M' == 5MiB)", + "falls back to the hardcoded default cap when SUPABASE_SCANNER_BUFFER_SIZE is set but unparseable (not '5M' == 5MiB)", () => Effect.gen(function* () { const { workdir } = yield* schemaFile( @@ -1080,51 +1076,47 @@ layer(BunServices.layer)("applySchemaFiles", (it) => { ); const { session } = fakeSession(); const error = yield* applySchema(session, workdir, "supabase/big.sql").pipe(Effect.flip); - expect(error.message).toContain("bufio.Scanner: token too long"); + expect(error.message).toContain("scanner: token too long"); expect(error.message).toContain( "Try setting SUPABASE_SCANNER_BUFFER_SIZE=5MB (current size is 256KB)", ); }).pipe((body) => withConfigEnv({ SUPABASE_SCANNER_BUFFER_SIZE: "5M" }, body)), ); - it.effect( - "accepts a hex-literal SUPABASE_SCANNER_BUFFER_SIZE (Go strconv.ParseInt base-0 parity, review CLI-1958)", - () => - Effect.gen(function* () { - const { workdir } = yield* schemaFile( - "schema-files-scanner-hex-", - "big.sql", - `SELECT 1;\nSELECT '${"a".repeat(5116)}';\n`, - ); - const { session } = fakeSession(); - const error = yield* applySchema(session, workdir, "supabase/big.sql").pipe(Effect.flip); - expect(error.message).toContain("bufio.Scanner: token too long"); - expect(error.message).toContain( - "Try setting SUPABASE_SCANNER_BUFFER_SIZE=5MB (current size is 5KB)", - ); - }).pipe((body) => withConfigEnv({ SUPABASE_SCANNER_BUFFER_SIZE: "0x1400" }, body)), + it.effect("accepts a hex-literal SUPABASE_SCANNER_BUFFER_SIZE", () => + Effect.gen(function* () { + const { workdir } = yield* schemaFile( + "schema-files-scanner-hex-", + "big.sql", + `SELECT 1;\nSELECT '${"a".repeat(5116)}';\n`, + ); + const { session } = fakeSession(); + const error = yield* applySchema(session, workdir, "supabase/big.sql").pipe(Effect.flip); + expect(error.message).toContain("scanner: token too long"); + expect(error.message).toContain( + "Try setting SUPABASE_SCANNER_BUFFER_SIZE=5MB (current size is 5KB)", + ); + }).pipe((body) => withConfigEnv({ SUPABASE_SCANNER_BUFFER_SIZE: "0x1400" }, body)), ); - it.effect( - "accepts underscore digit separators in a decimal SUPABASE_SCANNER_BUFFER_SIZE (Go strconv.ParseInt base-0 underscore-literal parity, review CLI-1958)", - () => - Effect.gen(function* () { - const { workdir } = yield* schemaFile( - "schema-files-scanner-underscore-", - "big.sql", - `SELECT 1;\nSELECT '${"a".repeat(5116)}';\n`, - ); - const { session } = fakeSession(); - const error = yield* applySchema(session, workdir, "supabase/big.sql").pipe(Effect.flip); - expect(error.message).toContain("bufio.Scanner: token too long"); - expect(error.message).toContain( - "Try setting SUPABASE_SCANNER_BUFFER_SIZE=5MB (current size is 5KB)", - ); - }).pipe((body) => withConfigEnv({ SUPABASE_SCANNER_BUFFER_SIZE: "5_120" }, body)), + it.effect("accepts underscore digit separators in a decimal SUPABASE_SCANNER_BUFFER_SIZE", () => + Effect.gen(function* () { + const { workdir } = yield* schemaFile( + "schema-files-scanner-underscore-", + "big.sql", + `SELECT 1;\nSELECT '${"a".repeat(5116)}';\n`, + ); + const { session } = fakeSession(); + const error = yield* applySchema(session, workdir, "supabase/big.sql").pipe(Effect.flip); + expect(error.message).toContain("scanner: token too long"); + expect(error.message).toContain( + "Try setting SUPABASE_SCANNER_BUFFER_SIZE=5MB (current size is 5KB)", + ); + }).pipe((body) => withConfigEnv({ SUPABASE_SCANNER_BUFFER_SIZE: "5_120" }, body)), ); it.effect( - "rejects an invalid underscore placement in SUPABASE_SCANNER_BUFFER_SIZE, unlike a valid digit separator (Go strconv.ParseInt underscore-grammar parity, review CLI-1958)", + "rejects an invalid underscore placement in SUPABASE_SCANNER_BUFFER_SIZE, unlike a valid digit separator", () => Effect.gen(function* () { const { workdir } = yield* schemaFile( @@ -1139,7 +1131,7 @@ layer(BunServices.layer)("applySchemaFiles", (it) => { ); it.effect( - "falls back to Go's hardcoded default cap when SUPABASE_SCANNER_BUFFER_SIZE overflows Go's signed int range (strconv.ParseInt/cast.ToInt range-error parity, review CLI-1958)", + "falls back to the hardcoded default cap when SUPABASE_SCANNER_BUFFER_SIZE overflows the signed int range", () => Effect.gen(function* () { const { workdir } = yield* schemaFile( @@ -1149,7 +1141,7 @@ layer(BunServices.layer)("applySchemaFiles", (it) => { ); const { session } = fakeSession(); const error = yield* applySchema(session, workdir, "supabase/big.sql").pipe(Effect.flip); - expect(error.message).toContain("bufio.Scanner: token too long"); + expect(error.message).toContain("scanner: token too long"); expect(error.message).toContain( "Try setting SUPABASE_SCANNER_BUFFER_SIZE=5MB (current size is 256KB)", ); @@ -1159,7 +1151,7 @@ layer(BunServices.layer)("applySchemaFiles", (it) => { ); it.effect( - "still accepts the exact int64 boundary magnitudes for SUPABASE_SCANNER_BUFFER_SIZE (Go strconv.ParseInt range-boundary parity, review CLI-1958)", + "still accepts the exact int64 boundary magnitudes for SUPABASE_SCANNER_BUFFER_SIZE", () => Effect.gen(function* () { const { workdir } = yield* schemaFile( @@ -1176,7 +1168,7 @@ layer(BunServices.layer)("applySchemaFiles", (it) => { ); it.effect( - "rejects an oversized statement when SUPABASE_SCANNER_BUFFER_SIZE is set only in the project env (Go loadNestedEnv parity)", + "rejects an oversized statement when SUPABASE_SCANNER_BUFFER_SIZE is set only in the project env", () => Effect.gen(function* () { const { workdir } = yield* schemaFile( @@ -1188,27 +1180,25 @@ layer(BunServices.layer)("applySchemaFiles", (it) => { const error = yield* applySchema(session, workdir, "supabase/big.sql", { SUPABASE_SCANNER_BUFFER_SIZE: "100b", }).pipe(Effect.flip); - expect(error.message).toContain("bufio.Scanner: token too long"); + expect(error.message).toContain("scanner: token too long"); }).pipe(withEmptyConfigEnv), ); - it.effect( - "shell env still wins over the project env for SUPABASE_SCANNER_BUFFER_SIZE (Go godotenv 'never overrides' parity)", - () => - Effect.gen(function* () { - const { workdir } = yield* schemaFile( - "schema-files-scanner-shellwins-", - "big.sql", - `SELECT '${"a".repeat(5000)}';\n`, - ); - const { session, calls } = fakeSession(); - // "0" is treated as unset (no check); the shell value must still win over the project - // env's tiny limit. - yield* applySchema(session, workdir, "supabase/big.sql", { - SUPABASE_SCANNER_BUFFER_SIZE: "100b", - }); - expect(executedSql(calls).some((sql) => sql.startsWith("SELECT 'a"))).toBe(true); - }).pipe((body) => withConfigEnv({ SUPABASE_SCANNER_BUFFER_SIZE: "0" }, body)), + it.effect("shell env still wins over the project env for SUPABASE_SCANNER_BUFFER_SIZE", () => + Effect.gen(function* () { + const { workdir } = yield* schemaFile( + "schema-files-scanner-shellwins-", + "big.sql", + `SELECT '${"a".repeat(5000)}';\n`, + ); + const { session, calls } = fakeSession(); + // "0" is treated as unset (no check); the shell value must still win over the project + // env's tiny limit. + yield* applySchema(session, workdir, "supabase/big.sql", { + SUPABASE_SCANNER_BUFFER_SIZE: "100b", + }); + expect(executedSql(calls).some((sql) => sql.startsWith("SELECT 'a"))).toBe(true); + }).pipe((body) => withConfigEnv({ SUPABASE_SCANNER_BUFFER_SIZE: "0" }, body)), ); it.effect( @@ -1231,20 +1221,19 @@ layer(BunServices.layer)("applySchemaFiles", (it) => { ); it.effect( - "fails with a read-phase error when SUPABASE_SCANNER_BUFFER_SIZE cannot be resolved", + "treats an unresolvable shell SUPABASE_SCANNER_BUFFER_SIZE as unset and falls back to the project env", () => Effect.gen(function* () { const { workdir } = yield* schemaFile( "schema-files-scanner-config-error-", "big.sql", - "SELECT 1;\n", - ); - const { session, calls } = fakeSession(); - const error = yield* applySchema(session, workdir, "supabase/big.sql").pipe(Effect.flip); - expect(error.message).toBe( - "failed to resolve environment variable: SUPABASE_SCANNER_BUFFER_SIZE", + `SELECT 1;\nSELECT '${"a".repeat(5000)}';\n`, ); - expect(executedSql(calls)).toEqual([]); + const { session } = fakeSession(); + const error = yield* applySchema(session, workdir, "supabase/big.sql", { + SUPABASE_SCANNER_BUFFER_SIZE: "100b", + }).pipe(Effect.flip); + expect(error.message).toContain("scanner: token too long"); }).pipe( Effect.provide( ConfigProvider.layer( diff --git a/apps/cli/src/command-internal/migration-checks-policy.ts b/apps/cli/src/command-internal/migration-checks-policy.ts new file mode 100644 index 0000000000..bb8380b174 --- /dev/null +++ b/apps/cli/src/command-internal/migration-checks-policy.ts @@ -0,0 +1,32 @@ +import type { CliConfig } from "@supabase/config"; +import { + DEFAULT_MIGRATION_CHECK_SEVERITY, + MIGRATION_CHECK_RULES, + mapMigrationCheckRules, + type MigrationCheckRule, + type MigrationCheckSeverity, +} from "@supabase/config/internal"; + +type MigrationChecksConfig = CliConfig["db"]["migrations"]["checks"]; + +/** One object a rule skips, written "schema.name". */ +interface MigrationCheckExclusion { + readonly rule: MigrationCheckRule; + readonly object: string; +} + +/** The migration check policy in the shape `@supabase/pg-checks` takes. */ +export interface MigrationChecksPolicy { + readonly severities: { readonly [R in MigrationCheckRule]: MigrationCheckSeverity }; + readonly exclude: ReadonlyArray; +} + +/** Resolves `[db.migrations.checks]`: an unset rule is "warn", exclusions become rule/object pairs. */ +export const resolveMigrationChecksPolicy = ( + checks: MigrationChecksConfig, +): MigrationChecksPolicy => ({ + severities: mapMigrationCheckRules((rule) => checks?.[rule] ?? DEFAULT_MIGRATION_CHECK_SEVERITY), + exclude: MIGRATION_CHECK_RULES.flatMap((rule) => + (checks?.exclude?.[rule] ?? []).map((object) => ({ rule, object })), + ), +}); diff --git a/apps/cli/src/command-internal/migration-checks-policy.unit.test.ts b/apps/cli/src/command-internal/migration-checks-policy.unit.test.ts new file mode 100644 index 0000000000..ec720d8327 --- /dev/null +++ b/apps/cli/src/command-internal/migration-checks-policy.unit.test.ts @@ -0,0 +1,60 @@ +import type { CliConfig } from "@supabase/config"; +import { describe, expect, it } from "vitest"; +import { resolveMigrationChecksPolicy } from "./migration-checks-policy.ts"; + +const warnSeverities = { + rls_disabled_in_public: "warn", + rls_enabled_no_policy: "warn", + policy_exists_rls_disabled: "warn", + security_definer_view: "warn", + rls_policy_always_true: "warn", + sensitive_columns_exposed: "warn", + auth_users_exposed: "warn", + function_search_path_mutable: "warn", + rls_references_user_metadata: "warn", +}; + +describe("resolveMigrationChecksPolicy", () => { + it("defaults all nine rules to warn with no exclusions when checks is absent", () => { + expect(resolveMigrationChecksPolicy(undefined)).toEqual({ + severities: warnSeverities, + exclude: [], + }); + }); + + it("keeps explicit severities and fills only unset rules with warn", () => { + const checks: CliConfig["db"]["migrations"]["checks"] = { + rls_disabled_in_public: "error", + rls_enabled_no_policy: "off", + }; + + expect(resolveMigrationChecksPolicy(checks)).toEqual({ + severities: { + ...warnSeverities, + rls_disabled_in_public: "error", + rls_enabled_no_policy: "off", + }, + exclude: [], + }); + }); + + it("flattens exclusions in rule order while keeping entry order", () => { + const checks: CliConfig["db"]["migrations"]["checks"] = { + exclude: { + function_search_path_mutable: ["public.z_function", "public.a_function"], + security_definer_view: [], + rls_disabled_in_public: ["public.z_table", "public.a_table"], + }, + }; + + expect(resolveMigrationChecksPolicy(checks)).toEqual({ + severities: warnSeverities, + exclude: [ + { rule: "rls_disabled_in_public", object: "public.z_table" }, + { rule: "rls_disabled_in_public", object: "public.a_table" }, + { rule: "function_search_path_mutable", object: "public.z_function" }, + { rule: "function_search_path_mutable", object: "public.a_function" }, + ], + }); + }); +}); diff --git a/apps/cli/src/command-internal/migration-fetch-run.ts b/apps/cli/src/command-internal/migration-fetch-run.ts index 2e6c480f8b..45bf6ec894 100644 --- a/apps/cli/src/command-internal/migration-fetch-run.ts +++ b/apps/cli/src/command-internal/migration-fetch-run.ts @@ -4,30 +4,21 @@ import { DnsResolverFlag, resolveYesWithProjectEnv } from "./global-flags.ts"; import { CONTEXT_CANCELED_MESSAGE } from "../shared/output/errors.ts"; import { Output } from "../shared/output/output.service.ts"; import { CommandSettings } from "../config/command-settings.service.ts"; -import { RemoteTarget } from "../config/remote-target.service.ts"; import { bold } from "./colors.ts"; import { DbConfigResolver } from "./db-config.service.ts"; import { DbConnection } from "./db-connection.service.ts"; -import { loadProjectEnv } from "./db-config.toml-read.ts"; -import { type DbTargetSelection } from "./db-target-flags.ts"; +import { loadProjectEnvValues } from "./db-config.toml-read.ts"; +import { announcedTarget, hostedMutationPreapproval } from "./hosted-consent/hosted-consent.ts"; import { readMigrationTable } from "./migration-history.ts"; +import { promptYesNo } from "./prompt-yes-no.ts"; +import type { ResolvedTarget } from "./target/resolve-target.ts"; import { LinkedProjectCache } from "../telemetry/linked-project-cache.service.ts"; -import { - MigrationTargetFlagsError, - OperationCanceledError, -} from "../commands/migration/migration.errors.ts"; -import { migrationConfirm } from "../commands/migration/migration.prompt.ts"; -import type { MigrationFetchFlags } from "../commands/migration/fetch/fetch.command.ts"; +import { OperationCanceledError } from "../commands/migration/migration.errors.ts"; import { MigrationFetchWriteError } from "../commands/migration/fetch/fetch.errors.ts"; -// Re-exported so an in-process caller (`pull`) can build a `MigrationFetchInput` without -// reaching into `commands/migration/**` directly. -export type { MigrationFetchFlags }; - export interface MigrationFetchInput { - readonly flags: MigrationFetchFlags; - readonly target: DbTargetSelection; - /** Overrides `--yes`/`SUPABASE_YES`/`supabase/.env` resolution. */ + readonly target: ResolvedTarget; + /** Overrides `--yes`/`SUPABASE_YES`/`supabase/.env` resolution for the overwrite prompt. */ readonly assumeYes?: boolean; } @@ -39,7 +30,7 @@ export interface MigrationFetchOutcome { export const runMigrationFetch = Effect.fn("MigrationFetch.run")(function* ( input: MigrationFetchInput, ) { - const { flags, target, assumeYes } = input; + const { target, assumeYes } = input; const output = yield* Output; const resolver = yield* DbConfigResolver; const connection = yield* DbConnection; @@ -48,62 +39,30 @@ export const runMigrationFetch = Effect.fn("MigrationFetch.run")(function* ( const path = yield* Path.Path; const dnsResolver = yield* DnsResolverFlag; - // Validated first, before any other work — same ordering as `migration down`/`repair`. - if (target.setFlags.length > 1) { - return yield* new MigrationTargetFlagsError({ - message: `if any flags in the group [db-url linked local] are set none of the others can be; [${target.setFlags.join(" ")}] were all set`, - }); - } - - const connType = target.connType ?? "linked"; // fetch defaults to `--linked`. - yield* Effect.annotateCurrentSpan("db.conn_type", connType); + yield* Effect.annotateCurrentSpan("db.conn_type", target.kind); - // `--project-ref` never implies `--linked` and must not be silently - // discarded on a non-linked target — see push.handler.ts's identical guard - // (db push) for the full TS-only rationale. - if (Option.isSome(flags.projectRef) && connType !== "linked") { - return yield* new MigrationTargetFlagsError({ - message: - "--project-ref only applies when targeting the linked project; use it with --linked (not --local or --db-url)", - }); - } - - // Resolve the DB config BEFORE any filesystem/prompt side effects — an invalid - // `--db-url`/`config.toml` then fails immediately, instead of first creating - // `supabase/migrations` or letting a declined overwrite prompt mask the real error - // with `context canceled`. Same fix as `migration repair`. - const remoteTarget = yield* RemoteTarget; - const linkedTarget = - connType === "linked" ? yield* remoteTarget.load(flags.projectRef) : undefined; - const cfg = yield* resolver.resolve({ - dbUrl: flags.dbUrl, - connType, + // Resolved before any filesystem or prompt side effect so an invalid `--db-url` or + // `config.toml` fails immediately instead of behind a declined overwrite prompt. + const cfg = yield* resolver.resolveConnection({ + target, dnsResolver, - linkedProjectRef: flags.projectRef, - target: linkedTarget, + password: Option.none(), + networkBanRemoval: yield* hostedMutationPreapproval, }); - // The project .env loads after the parse-time flag-group validation above — so a - // SUPABASE_YES set only in supabase/.env auto-confirms, but a flag conflict still - // surfaces before any .env read. Resolve --yes against the project env here, not - // just process.env. Same ordering as `migration down`/`repair`. - const projectEnv = yield* loadProjectEnv(fs, path, cliSettings.workdir); + const projectEnv = yield* loadProjectEnvValues(fs, path, cliSettings.workdir); const yes = yield* resolveYesWithProjectEnv(projectEnv); - // Linked fetch caches the project ref on success. The cache write is attached to the body - // via `Effect.ensuring`, so a declined prompt returns before it runs. + // The cache write is attached to the body so a declined prompt returns before it runs. let cacheLinkedRef: Effect.Effect | undefined; - if (linkedTarget !== undefined) { + if (target.kind === "linked") { const linkedProjectCache = yield* LinkedProjectCache; - cacheLinkedRef = linkedProjectCache.cache(linkedTarget.ref); + cacheLinkedRef = linkedProjectCache.cache(target.ref); } const fetchBody = Effect.gen(function* () { const migrationsDir = path.join(cliSettings.workdir, "supabase", "migrations"); - yield* fs - .makeDirectory(migrationsDir, { recursive: true }) - .pipe(Effect.mapError((cause) => new MigrationFetchWriteError({ message: cause.message }))); // Only a missing directory counts as "empty"; any other read error must propagate — // collapsing it to empty would skip the confirmation and risk clobbering existing migrations. const existing = yield* fs.readDirectory(migrationsDir).pipe( @@ -118,11 +77,15 @@ export const runMigrationFetch = Effect.fn("MigrationFetch.run")(function* ( ), ); if (existing.length > 0) { - const title = `Do you want to overwrite existing files in ${bold("supabase/migrations")} directory?`; + const source = Option.match(yield* announcedTarget, { + onNone: () => "the target database", + onSome: bold, + }); + const title = `Overwrite local migrations with the history from ${source}?`; const overwrite = assumeYes !== undefined ? assumeYes - : yield* migrationConfirm(title, { defaultValue: true, yes }); + : yield* promptYesNo(output, yes, title, true, true, { readMachineStdin: true }); if (!overwrite) { return yield* new OperationCanceledError({ message: CONTEXT_CANCELED_MESSAGE }); } @@ -130,12 +93,6 @@ export const runMigrationFetch = Effect.fn("MigrationFetch.run")(function* ( const migrations = yield* Effect.scoped( Effect.gen(function* () { - // The connect diagnostic prints to stderr before dialing, - // local/remote per the resolved connection. - yield* output.raw( - `Connecting to ${cfg.isLocal ? "local" : "remote"} database...\n`, - "stderr", - ); const session = yield* connection.connect(cfg.conn, { isLocal: cfg.isLocal, dnsResolver, @@ -144,6 +101,10 @@ export const runMigrationFetch = Effect.fn("MigrationFetch.run")(function* ( }), ); + yield* fs + .makeDirectory(migrationsDir, { recursive: true }) + .pipe(Effect.mapError((cause) => new MigrationFetchWriteError({ message: cause.message }))); + const written: Array = []; for (const file of migrations) { // The version/name come from the remote `schema_migrations` table. A hostile remote could diff --git a/apps/cli/src/command-internal/migration-history.ts b/apps/cli/src/command-internal/migration-history.ts index 3ff20c7f21..1089e149f0 100644 --- a/apps/cli/src/command-internal/migration-history.ts +++ b/apps/cli/src/command-internal/migration-history.ts @@ -171,7 +171,7 @@ export type MigrationSync = export function reconcileMigrations( remote: ReadonlyArray, local: ReadonlyArray, - isLocal = false, + targetFlags: string, ): MigrationSync { // `MIGRATION_VERSION_MAX` pins the exhausted side of the walk; `parseMigrationVersion` is // shared with `migration list` so both surfaces skip the same edge-case versions. @@ -216,7 +216,7 @@ export function reconcileMigrations( if (extraRemote.length + extraLocal.length > 0) { return { kind: "conflict", - suggestion: suggestMigrationRepair(extraRemote, extraLocal, isLocal), + suggestion: suggestMigrationRepair(extraRemote, extraLocal, targetFlags), }; } if (local.length === 0) { @@ -225,31 +225,32 @@ export function reconcileMigrations( return { kind: "in-sync" }; } +export const withTargetFlags = (command: string, targetFlags: string): string => + targetFlags === "" ? command : `${command} ${targetFlags}`; + export function suggestMigrationRepair( extraRemote: ReadonlyArray, extraLocal: ReadonlyArray, - isLocal = false, + targetFlags: string, ): string { - const localFlag = isLocal ? " --local" : ""; let result = "\nMake sure your local git repo is up-to-date. If the error persists, try repairing the migration history table:\n"; for (const version of extraRemote) { - result += `${bold(`supabase migration repair${localFlag} --status reverted ${version}`)}\n`; + result += `${bold(withTargetFlags(`supabase migration repair --status reverted ${version}`, targetFlags))}\n`; } for (const version of extraLocal) { - result += `${bold(`supabase migration repair${localFlag} --status applied ${version}`)}\n`; + result += `${bold(withTargetFlags(`supabase migration repair --status applied ${version}`, targetFlags))}\n`; } return result; } /** Each generated line ends with a trailing newline, including the last one. */ -export function suggestRevertHistory(versions: ReadonlyArray, isLocal = false): string { - const localFlag = isLocal ? " --local" : ""; +export function suggestRevertHistory(versions: ReadonlyArray, targetFlags: string): string { return ( "\nMake sure your local git repo is up-to-date. If the error persists, try repairing the migration history table:\n" + - `${bold(`supabase migration repair${localFlag} --status reverted ${versions.join(" ")}`)}\n` + + `${bold(withTargetFlags(`supabase migration repair --status reverted ${versions.join(" ")}`, targetFlags))}\n` + "\nAnd update local migrations to match remote database:\n" + - `${bold(`supabase db pull${localFlag}`)}\n` + `${bold(withTargetFlags("supabase db pull", targetFlags))}\n` ); } diff --git a/apps/cli/src/command-internal/migration-history.unit.test.ts b/apps/cli/src/command-internal/migration-history.unit.test.ts index 2c37b61abe..95507b05f2 100644 --- a/apps/cli/src/command-internal/migration-history.unit.test.ts +++ b/apps/cli/src/command-internal/migration-history.unit.test.ts @@ -29,64 +29,76 @@ const failingSession = (error: DbExecError): DbSession => ({ describe("reconcileMigrations", () => { it("reports in-sync when remote and local match", () => { - expect(reconcileMigrations(["20240101000000"], ["20240101000000"])).toEqual({ + expect(reconcileMigrations(["20240101000000"], ["20240101000000"], "--remote")).toEqual({ kind: "in-sync", }); }); it("reports missing only when both histories are empty", () => { - expect(reconcileMigrations([], [])).toEqual({ kind: "missing" }); - expect(reconcileMigrations(["20240101000000"], []).kind).toBe("conflict"); + expect(reconcileMigrations([], [], "--remote")).toEqual({ kind: "missing" }); + expect(reconcileMigrations(["20240101000000"], [], "--remote").kind).toBe("conflict"); }); it("reports a conflict with an extra remote migration", () => { - const result = reconcileMigrations(["20240101000000"], ["20240102000000"]); + const result = reconcileMigrations(["20240101000000"], ["20240102000000"], "--remote"); expect(result.kind).toBe("conflict"); if (result.kind === "conflict") { expect(stripAnsi(result.suggestion)).toContain( - "supabase migration repair --status reverted 20240101000000", + "supabase migration repair --status reverted 20240101000000 --remote", ); expect(stripAnsi(result.suggestion)).toContain( - "supabase migration repair --status applied 20240102000000", + "supabase migration repair --status applied 20240102000000 --remote", ); } }); it("reports a conflict with an extra local migration", () => { - const result = reconcileMigrations([], ["20240102000000"]); + const result = reconcileMigrations([], ["20240102000000"], "--remote"); expect(result.kind).toBe("conflict"); }); it("is in sync when an 8-digit and a 14-digit version share a prefix (#6036)", () => { expect( - reconcileMigrations(["20260420", "20260420010000"], ["20260420010000", "20260420"]), + reconcileMigrations( + ["20260420", "20260420010000"], + ["20260420010000", "20260420"], + "--remote", + ), ).toEqual({ kind: "in-sync" }); }); it("skips versions that do not parse as integers", () => { - expect(reconcileMigrations(["bogus", "20240101000000"], ["20240101000000"])).toEqual({ + expect( + reconcileMigrations(["bogus", "20240101000000"], ["20240101000000"], "--remote"), + ).toEqual({ kind: "in-sync", }); }); - it("skips empty / whitespace versions (matches strconv.Atoi, not Number())", () => { - expect(reconcileMigrations(["", "20240101000000"], [" ", "20240101000000"])).toEqual({ + it("skips empty / whitespace versions (not Number() coercion)", () => { + expect( + reconcileMigrations(["", "20240101000000"], [" ", "20240101000000"], "--remote"), + ).toEqual({ kind: "in-sync", }); }); - it("treats a version within Go's int64 range as a real conflict (BigInt parity)", () => { - expect(reconcileMigrations(["9999999999999999"], []).kind).toBe("conflict"); + it("treats a version within the int64 range as a real conflict", () => { + expect(reconcileMigrations(["9999999999999999"], [], "--remote").kind).toBe("conflict"); }); - it("skips a version beyond Go's int64 range instead of hanging the scan", () => { + it("skips a version beyond the int64 range instead of hanging the scan", () => { expect( - reconcileMigrations(["20240101000000", "9999999999999999999"], ["20240101000000"]), + reconcileMigrations( + ["20240101000000", "9999999999999999999"], + ["20240101000000"], + "--remote", + ), ).toEqual({ kind: "in-sync" }); }); }); -describe("listRemoteMigrations (suppress only undefined_table, like Go)", () => { +describe("listRemoteMigrations (suppress only undefined_table)", () => { const run = (error: DbExecError) => listRemoteMigrations(failingSession(error)).pipe(Effect.exit); it.effect("treats a missing history table (42P01) as an empty history", () => @@ -129,7 +141,7 @@ describe("listRemoteMigrations (suppress only undefined_table, like Go)", () => ); }); -describe("findPendingMigrations (Go TestPendingMigrations / TestIgnoreVersionMismatch)", () => { +describe("findPendingMigrations", () => { it("returns the local paths after the remote count when in sync", () => { const local = ["0", "1", "2"].map(mig); const result = findPendingMigrations(local, ["0"]); @@ -174,25 +186,33 @@ describe("findPendingMigrations (Go TestPendingMigrations / TestIgnoreVersionMis }); describe("suggestMigrationRepair", () => { - it("lists reverted (remote) then applied (local) repair commands", () => { - const out = stripAnsi(suggestMigrationRepair(["111"], ["222"])); + it("lists reverted (remote) then applied (local) repair commands for the target", () => { + const out = stripAnsi(suggestMigrationRepair(["111"], ["222"], "--remote")); expect(out).toContain("try repairing the migration history table:"); - expect(out).toContain("supabase migration repair --status reverted 111"); - expect(out).toContain("supabase migration repair --status applied 222"); + expect(out).toContain("supabase migration repair --status reverted 111 --remote"); + expect(out).toContain("supabase migration repair --status applied 222 --remote"); + }); + + it("puts the target last in every repair command, so --remote cannot swallow a version", () => { + expect(stripAnsi(suggestMigrationRepair(["111"], [], "--local"))).toContain( + "supabase migration repair --status reverted 111 --local", + ); + expect(stripAnsi(suggestMigrationRepair([], ["222"], "--remote="))).toContain( + "supabase migration repair --status applied 222 --remote=", + ); }); }); describe("suggestRevertHistory", () => { it("builds the revert-history suggestion with a trailing newline per line", () => { - expect(suggestRevertHistory(["0002", "0003"])).toContain( - "supabase migration repair --status reverted 0002 0003", - ); - expect(suggestRevertHistory(["0002"])).toMatch(/\n$/u); - expect(suggestRevertHistory(["0002"])).toContain("supabase db pull"); + const out = stripAnsi(suggestRevertHistory(["0002", "0003"], "--remote")); + expect(out).toContain("supabase migration repair --status reverted 0002 0003 --remote"); + expect(out).toContain("supabase db pull --remote"); + expect(out).toMatch(/\n$/u); }); }); -describe("resolveMigrationFile (byte-ordered match, Go's sort.Strings via afero match.go:91)", () => { +describe("resolveMigrationFile (byte-ordered match)", () => { it.effect("picks the UTF-8-byte-first match, not JS's default UTF-16 code-unit order", () => { // A supplementary-plane character (U+1F600, a UTF-16 surrogate pair) alongside a BMP // private-use character (U+E000): JS's default `.sort()` ranks the surrogate pair first — diff --git a/apps/cli/src/command-internal/migration-timestamp.format.unit.test.ts b/apps/cli/src/command-internal/migration-timestamp.format.unit.test.ts index c3e1d2114b..47bfd4dcd5 100644 --- a/apps/cli/src/command-internal/migration-timestamp.format.unit.test.ts +++ b/apps/cli/src/command-internal/migration-timestamp.format.unit.test.ts @@ -11,7 +11,7 @@ describe("formatTimestampVersion", () => { expect(formatTimestampVersion("20220727064246")).toBe("2022-07-27 06:42:46"); }); - it("passes through short numeric versions (Go time.Parse errors → input)", () => { + it("passes through short numeric versions (parse errors → input)", () => { expect(formatTimestampVersion("0")).toBe("0"); expect(formatTimestampVersion("1")).toBe("1"); }); @@ -20,7 +20,7 @@ describe("formatTimestampVersion", () => { expect(formatTimestampVersion("abc")).toBe("abc"); }); - it("passes through out-of-range months/days like Go's strict parse", () => { + it("passes through out-of-range months/days", () => { expect(formatTimestampVersion("20221327064246")).toBe("20221327064246"); // month 13 expect(formatTimestampVersion("20220230000000")).toBe("20220230000000"); // Feb 30 expect(formatTimestampVersion("20220727256046")).toBe("20220727256046"); // hour 25 @@ -42,7 +42,7 @@ describe("parseMigrationVersion", () => { expect(parseMigrationVersion("0")).toBe(0n); }); - it("rejects non-integer versions like Go's strconv.Atoi", () => { + it("rejects non-integer versions", () => { expect(parseMigrationVersion("")).toBeUndefined(); expect(parseMigrationVersion("abc")).toBeUndefined(); expect(parseMigrationVersion(" 12")).toBeUndefined(); @@ -51,17 +51,17 @@ describe("parseMigrationVersion", () => { expect(parseMigrationVersion("+")).toBeUndefined(); }); - it("accepts a leading +/- sign like strconv.Atoi (for malformed history rows)", () => { - // Go's `Atoi` == `ParseInt(s, 10, 0)` accepts a leading sign, so `migration repair -1` + it("accepts a leading +/- sign (for malformed history rows)", () => { + // A leading sign is accepted, so `migration repair -1` // can delete a malformed `-1` row and the two-pointer merge orders `-1` before `0`. expect(parseMigrationVersion("-1")).toBe(-1n); expect(parseMigrationVersion("+5")).toBe(5n); }); it("accepts the full int64 range but rejects values outside it", () => { - // 16 digits Go accepts and surfaces as a conflict — must NOT be skipped. + // 16 digits parse and surface as a conflict — must NOT be skipped. expect(parseMigrationVersion("9999999999999999")).toBe(9999999999999999n); - // int64 max/min parse; one beyond either bound is rejected (Go's Atoi range error). + // int64 max/min parse; one beyond either bound is rejected. expect(parseMigrationVersion(MIGRATION_VERSION_MAX.toString())).toBe(MIGRATION_VERSION_MAX); expect(parseMigrationVersion((MIGRATION_VERSION_MAX + 1n).toString())).toBeUndefined(); expect(parseMigrationVersion("-9223372036854775808")).toBe(-9223372036854775808n); diff --git a/apps/cli/src/command-internal/go-output-flag.ts b/apps/cli/src/command-internal/output-formats.ts similarity index 89% rename from apps/cli/src/command-internal/go-output-flag.ts rename to apps/cli/src/command-internal/output-formats.ts index 8e6ad00f44..52d4e85d8b 100644 --- a/apps/cli/src/command-internal/go-output-flag.ts +++ b/apps/cli/src/command-internal/output-formats.ts @@ -20,7 +20,7 @@ export const QUERY_OUTPUT_FORMATS = ["json", "table", "csv"] as const; /** * Raised when `-o`/`--output` carries a value the active command doesn't accept. The message - * matches the established pflag rejection format: `invalid argument %q for %q flag: %v`. + * has the form `invalid argument "" for "" flag: `. */ export class InvalidOutputFormatError extends Data.TaggedError("InvalidOutputFormatError")<{ readonly message: string; @@ -36,8 +36,8 @@ export function outputFormatEnumMessage(allowed: ReadonlyArray): string } /** - * The full rejection message for an invalid `-o` value, matching the established - * `invalid argument %q for %q flag: %v` pflag format. + * The full rejection message for an invalid `-o` value, in the + * `invalid argument "" for "" flag: ` format. */ export function invalidOutputFormatMessage(value: string, allowed: ReadonlyArray): string { return `invalid argument "${value}" for "-o, --output" flag: ${outputFormatEnumMessage(allowed)}`; diff --git a/apps/cli/src/command-internal/go-output-flag.unit.test.ts b/apps/cli/src/command-internal/output-formats.unit.test.ts similarity index 84% rename from apps/cli/src/command-internal/go-output-flag.unit.test.ts rename to apps/cli/src/command-internal/output-formats.unit.test.ts index 41b440f43f..d39cc59273 100644 --- a/apps/cli/src/command-internal/go-output-flag.unit.test.ts +++ b/apps/cli/src/command-internal/output-formats.unit.test.ts @@ -5,10 +5,10 @@ import { invalidOutputFormatMessage, outputFormatEnumMessage, unsupportedOutputFlagMessage, -} from "./go-output-flag.ts"; +} from "./output-formats.ts"; -describe("go-output-flag", () => { - it("joins the allowed set with Go's ` | ` bracket format", () => { +describe("output-formats", () => { + it("joins the allowed set with the ` | ` bracket format", () => { expect(outputFormatEnumMessage(RESOURCE_OUTPUT_FORMATS)).toBe( "must be one of [ env | pretty | json | toml | yaml ]", ); @@ -17,8 +17,7 @@ describe("go-output-flag", () => { ); }); - it("reproduces Go's pflag rejection message byte-for-byte", () => { - // pflag: `invalid argument %q for %q flag: %v`, shorthand-prefixed `-o, --output`. + it("formats the rejection message with the shorthand-prefixed flag name", () => { expect(invalidOutputFormatMessage("table", RESOURCE_OUTPUT_FORMATS)).toBe( 'invalid argument "table" for "-o, --output" flag: must be one of [ env | pretty | json | toml | yaml ]', ); diff --git a/apps/cli/src/command-internal/go-output.encoders.ts b/apps/cli/src/command-internal/output.encoders.ts similarity index 78% rename from apps/cli/src/command-internal/go-output.encoders.ts rename to apps/cli/src/command-internal/output.encoders.ts index 8a026f7816..c7adfb14ec 100644 --- a/apps/cli/src/command-internal/go-output.encoders.ts +++ b/apps/cli/src/command-internal/output.encoders.ts @@ -1,17 +1,17 @@ import { stringify as stringifyToml } from "smol-toml"; import { stringify as stringifyYaml } from "yaml"; -import { encodeGoJsonCompact, encodeGoJsonIndented } from "./go-json.ts"; -import { goStringCompare } from "./go-struct-output.encoders.ts"; +import { encodeHtmlSafeJsonCompact, encodeHtmlSafeJsonIndented } from "./html-safe-json.ts"; +import { compareByCodepoint } from "./struct-output.encoders.ts"; /** - * Reproduces `json.Encoder` output for `-o json`: alphabetical key order, Go string escaping, and + * Encodes `-o json` output: alphabetical key order, HTML-safe string escaping, and * a trailing newline. * * `nullForEmptyArrays` re-substitutes `null` for an empty array at the listed keys, for a schema * that decodes both `null` and `[]` to `[]` upstream (e.g. `backups list`'s `"backups": null`). */ -export function encodeGoJson( +export function encodeSortedJson( value: T, options?: { readonly nullForEmptyArrays?: ReadonlyArray }, ): string { @@ -33,24 +33,24 @@ export function encodeGoJson( } source = patched; } - return encodeGoJsonIndented(sortKeysDeep(source)); + return encodeHtmlSafeJsonIndented(sortKeysDeep(source)); } function sortKeysDeep(value: unknown): unknown { if (Array.isArray(value)) return value.map(sortKeysDeep); if (value === null || typeof value !== "object") return value; // A plain object reorders integer-like string keys ("2", "10") into ascending numeric order on - // enumeration; building a `Map` instead carries a lexicographic sort through to `go-json.ts`'s + // enumeration; building a `Map` instead carries a lexicographic sort through to `html-safe-json.ts`'s // `walk` intact, since `Map` iteration order is true insertion order. const sorted = new Map(); - // JS's default string sort orders by UTF-16 code unit, which diverges from Go's byte/codepoint + // JS's default string sort orders by UTF-16 code unit, which diverges from byte/codepoint // order once an astral character (a UTF-16 surrogate pair) meets a high-BMP one. - // `goStringCompare` reproduces Go's real map-key order instead — this matters here since `gen + // `compareByCodepoint` sorts by byte/codepoint order instead — this matters here since `gen // bearer-jwt`'s custom claims flow through this same sort before signing. - for (const key of Object.keys(value as Record).sort(goStringCompare)) { + for (const key of Object.keys(value as Record).sort(compareByCodepoint)) { const child = (value as Record)[key]; - // Drop `undefined` properties here, matching `JSON.stringify`'s behavior (the Go-faithful - // walker below would otherwise render them as `null`). + // Drop `undefined` properties here, matching `JSON.stringify`'s behavior (the walker + // below would otherwise render them as `null`). if (child === undefined) continue; sorted.set(key, sortKeysDeep(child)); } @@ -58,27 +58,27 @@ function sortKeysDeep(value: unknown): unknown { } /** - * Serializes an outbound API request body with sorted keys, Go string escaping, no indentation, - * and no trailing newline, matching `json.Marshal`'s struct output. + * Serializes an outbound API request body with sorted keys, HTML-safe string escaping, no + * indentation, and no trailing newline. * - * {@link encodeGoJson} is the parallel for human-facing `--output json`. + * {@link encodeSortedJson} is the parallel for human-facing `--output json`. */ -export function encodeGoStructJsonBody(value: unknown): string { - return encodeGoJsonCompact(sortKeysDeep(value)); +export function encodeSortedJsonBody(value: unknown): string { + return encodeHtmlSafeJsonCompact(sortKeysDeep(value)); } /** * YAML for map payloads (`branches get` envs, `sso info`, `status`, `postgres-config`). Struct - * payloads must use `encodeGoYaml` in `go-struct-output.encoders.ts` instead, since Go's yaml.v3 - * derives keys from Go field names, not JSON tags. + * payloads must use `encodeStructYaml` in `struct-output.encoders.ts` instead, since YAML keys + * derive from struct field names, not JSON tags. */ export function encodeYaml(value: unknown): string { return stringifyYaml(value); } /** - * TOML for map payloads. Struct payloads must use `encodeGoToml` in - * `go-struct-output.encoders.ts` instead, since BurntSushi emits PascalCase Go field names with + * TOML for map payloads. Struct payloads must use `encodeStructToml` in + * `struct-output.encoders.ts` instead, since the struct output uses PascalCase field names with * 2-space table indentation. */ export function encodeToml(value: unknown): string { @@ -90,7 +90,7 @@ export function encodeToml(value: unknown): string { } /** - * Reproduces the established `godotenv.Marshal` byte shape for `--output env`. + * Emits the established dotenv byte shape for `--output env`. * * Nested maps flatten to dotted paths, then uppercase with `.` replaced by `_`. Flattening does * not descend into slices: an array value becomes a single empty-string leaf. Integer-parseable diff --git a/apps/cli/src/command-internal/go-output.encoders.unit.test.ts b/apps/cli/src/command-internal/output.encoders.unit.test.ts similarity index 81% rename from apps/cli/src/command-internal/go-output.encoders.unit.test.ts rename to apps/cli/src/command-internal/output.encoders.unit.test.ts index 98738ed362..f43e55018f 100644 --- a/apps/cli/src/command-internal/go-output.encoders.unit.test.ts +++ b/apps/cli/src/command-internal/output.encoders.unit.test.ts @@ -1,7 +1,7 @@ import { V1ListAllBackupsOutput } from "@supabase/api/effect"; import { describe, expect, it } from "vitest"; -import { encodeEnv, encodeGoJson, encodeToml, encodeYaml } from "./go-output.encoders.ts"; +import { encodeEnv, encodeSortedJson, encodeToml, encodeYaml } from "./output.encoders.ts"; // Shaped like the backups response because the `nullForEmptyArrays` byte-parity assertions were // extracted from that port; see the `{ items, name }` fixtures below for plain-object coverage. @@ -23,9 +23,9 @@ const SAMPLE_RESPONSE: typeof V1ListAllBackupsOutput.Type = { }, }; -describe("encodeGoJson", () => { - it("emits Go's alphabetical struct-field order and trailing newline for a populated response", () => { - const out = encodeGoJson(SAMPLE_RESPONSE, { nullForEmptyArrays: ["backups"] }); +describe("encodeSortedJson", () => { + it("emits alphabetical struct-field order and trailing newline for a populated response", () => { + const out = encodeSortedJson(SAMPLE_RESPONSE, { nullForEmptyArrays: ["backups"] }); expect(out).toBe( `{ "backups": [ @@ -49,7 +49,7 @@ describe("encodeGoJson", () => { }); it("emits backups: null and an empty physical_backup_data object for a PITR-only response", () => { - const out = encodeGoJson( + const out = encodeSortedJson( { region: "ap-southeast-1", walg_enabled: false, @@ -72,7 +72,7 @@ describe("encodeGoJson", () => { }); it("leaves arrays intact when nullForEmptyArrays is not provided", () => { - const out = encodeGoJson({ items: [], name: "x" }); + const out = encodeSortedJson({ items: [], name: "x" }); expect(out).toBe( `{ "items": [], @@ -83,7 +83,7 @@ describe("encodeGoJson", () => { }); it("does not substitute null for non-empty arrays even when listed in nullForEmptyArrays", () => { - const out = encodeGoJson({ items: [1, 2], name: "x" }, { nullForEmptyArrays: ["items"] }); + const out = encodeSortedJson({ items: [1, 2], name: "x" }, { nullForEmptyArrays: ["items"] }); expect(out).toBe( `{ "items": [ @@ -96,8 +96,8 @@ describe("encodeGoJson", () => { ); }); - it("keeps Go's true lexicographic order for numeric-looking keys (CLI-1961 Codex review finding)", () => { - const out = encodeGoJson({ 10: "a", 2: "b", role: "anon" }); + it("keeps true lexicographic order for numeric-looking keys", () => { + const out = encodeSortedJson({ 10: "a", 2: "b", role: "anon" }); expect(out).toBe( `{ "10": "a", @@ -108,10 +108,10 @@ describe("encodeGoJson", () => { ); }); - it("sorts keys by Go's byte/code-point order, not JS's UTF-16 code-unit order (CLI-1961 Codex review finding)", () => { + it("sorts keys by byte/code-point order, not JS's UTF-16 code-unit order", () => { const highBmp = String.fromCodePoint(0xe000); const astral = String.fromCodePoint(0x10000); - const out = encodeGoJson({ [astral]: 2, [highBmp]: 1 }); + const out = encodeSortedJson({ [astral]: 2, [highBmp]: 1 }); expect(out).toBe(`{\n "${highBmp}": 1,\n "${astral}": 2\n}\n`); }); }); @@ -145,21 +145,21 @@ describe("encodeEnv", () => { expect(lines).toContain('PITR_ENABLED="true"'); }); - it("emits integer-parseable values unquoted (matches godotenv strconv.Atoi branch)", () => { + it("emits integer-parseable values unquoted", () => { const out = encodeEnv(SAMPLE_RESPONSE); const lines = out.split("\n"); expect(lines).toContain("PHYSICAL_BACKUP_DATA_EARLIEST_PHYSICAL_BACKUP_DATE_UNIX=1700000000"); expect(lines).toContain("PHYSICAL_BACKUP_DATA_LATEST_PHYSICAL_BACKUP_DATE_UNIX=1700001000"); }); - it("collapses arrays to a single empty leaf (Go viper does not descend into slices)", () => { + it("collapses arrays to a single empty leaf", () => { const out = encodeEnv(SAMPLE_RESPONSE); const lines = out.split("\n"); expect(lines).toContain('BACKUPS=""'); expect(lines.some((line) => line.startsWith("BACKUPS_0_"))).toBe(false); }); - it("matches Go's full env output for the sample backup response", () => { + it("emits the full env output for the sample backup response", () => { expect(encodeEnv(SAMPLE_RESPONSE)).toBe( [ 'BACKUPS=""', @@ -177,7 +177,7 @@ describe("encodeEnv", () => { expect(out).toBe('MESSAGE="with \\"quotes\\" and \\\\backslash"'); }); - it("escapes embedded newlines, carriage returns, and tabs (Go %q parity)", () => { + it("escapes embedded newlines, carriage returns, and tabs", () => { const out = encodeEnv({ description: "line one\nline two\rwith\ttab" }); expect(out).toBe('DESCRIPTION="line one\\nline two\\rwith\\ttab"'); }); @@ -187,11 +187,11 @@ describe("encodeEnv", () => { expect(out.split("\n")).toEqual(["A=2", "M=3", "Z=1"]); }); - it("omits empty nested maps entirely (Go viper parity)", () => { + it("omits empty nested maps entirely", () => { expect(encodeEnv({ physical_backup_data: {} })).toBe(""); }); - it("matches Go for the PITR-only response shape with empty physical_backup_data", () => { + it("handles the PITR-only response shape with empty physical_backup_data", () => { expect( encodeEnv({ region: "ap-southeast-1", diff --git a/apps/cli/src/command-internal/parent-project-ref.ts b/apps/cli/src/command-internal/parent-project-ref.ts index 9bac16fb00..d1e9c05b30 100644 --- a/apps/cli/src/command-internal/parent-project-ref.ts +++ b/apps/cli/src/command-internal/parent-project-ref.ts @@ -1,18 +1,91 @@ +import { Effect, FileSystem, Option, Path } from "effect"; + +import { CommandSettings } from "../config/command-settings.service.ts"; +import { PROJECT_REF_PATTERN } from "../config/project-ref.service.ts"; +import { type CachedLinkedProject, readLinkSnapshotSoft } from "../shared/config/link-snapshot.ts"; + +export type ParentRefResolution = + | { + readonly kind: "resolved"; + readonly ref: string; + /** The linked ref when it differs from the recorded parent; the parent must list it. */ + readonly linkedRef?: string; + } + | { readonly kind: "invalid" } + | { readonly kind: "absent" }; + +/** The parent project the checkout's link records; any read failure is `None`. */ +export const readCachedLinkedProject = ( + fs: FileSystem.FileSystem, + path: Path.Path, + workdir: string, +): Effect.Effect> => + readLinkSnapshotSoft(fs, path, workdir).pipe(Effect.map((snapshot) => snapshot.parent)); + +function classifyParentCandidates( + candidates: ReadonlyArray>, +): ParentRefResolution { + // The first present candidate decides: a malformed higher-priority one stays invalid rather than + // falling through to a lower-priority project that a mutation could then hit by mistake. + for (const candidate of candidates) { + if (Option.isSome(candidate)) { + return PROJECT_REF_PATTERN.test(candidate.value) + ? { kind: "resolved", ref: candidate.value } + : { kind: "invalid" }; + } + } + return { kind: "absent" }; +} + /** - * A value made entirely of lowercase letters (but not 20 of them, or it would already have - * been treated as a ref) is a plausible ref typo. Appended to both {@link parentNotLinkedMessage} - * and `link`'s own branch-not-found message. + * The linked parent project ref, not the branch's own ref that `resolve` returns right after + * `link `: `SUPABASE_PROJECT_ID`, then the link's recorded parent, then the linked ref. */ +export const resolveLinkedParentRef = Effect.fn("ParentProjectRef.resolveLinked")(function* () { + const cliSettings = yield* CommandSettings; + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const snapshot = yield* readLinkSnapshotSoft(fs, path, cliSettings.workdir); + const linkedRef = snapshot.ref; + + // The recorded parent alone isn't proof a link completed — legacy `link` writes it on both + // success and failure — so it counts only when the linked ref confirms the link. + const cachedRef = Option.isSome(linkedRef) + ? Option.map(snapshot.parent, (parent) => parent.ref) + : Option.none(); + + // An env ref equal to the valid linked ref adds no parent information; drop it so the recorded + // parent wins. Pattern-valid only, so a garbage env equal to a garbage link stays invalid. + const envRef = + Option.isSome(cliSettings.projectId) && + Option.isSome(linkedRef) && + cliSettings.projectId.value === linkedRef.value && + PROJECT_REF_PATTERN.test(cliSettings.projectId.value) + ? Option.none() + : cliSettings.projectId; + + const parent = classifyParentCandidates([envRef, cachedRef, linkedRef]); + if (parent.kind !== "resolved" || Option.isSome(envRef) || Option.isNone(cachedRef)) + return parent; + const branchRef = Option.filter( + linkedRef, + (ref) => ref !== parent.ref && PROJECT_REF_PATTERN.test(ref), + ); + return Option.isSome(branchRef) ? { ...parent, linkedRef: branchRef.value } : parent; +}); + +/** A hint for a value that looks like a mistyped project ref; empty otherwise. */ export function parentRefTypoHint(value: string): string { - if (!/^[a-z]+$/.test(value)) return ""; - return `\n If you meant a project ref: refs are exactly 20 lowercase letters ("${value}" has ${value.length}).`; + if (/^[a-z]+$/.test(value)) { + return `\n If you meant a project ref: refs are exactly 20 lowercase letters ("${value}" has ${value.length}).`; + } + if (/^[A-Za-z]{20}$/.test(value)) { + return `\n If you meant a project ref: refs are exactly 20 lowercase letters (try "${value.toLowerCase()}").`; + } + return ""; } -/** - * Shared "no project is linked to search for branches" message, produced when - * `RemoteTarget.parent` reports `"absent"` for a non-ref-shaped - * `--project-ref`/positional value. Used by both `link` and `config diff`. - */ +/** The branch-name lookup error when no project is linked, shared by `link` and `config diff`. */ export function parentNotLinkedMessage(value: string): string { return ( `Cannot resolve "${value}": it is not a project ref (refs are exactly 20 lowercase letters, ` + @@ -23,11 +96,7 @@ export function parentNotLinkedMessage(value: string): string { ); } -/** - * Shared "the linked project ref is invalid" message, produced when - * `RemoteTarget.parent` reports `"invalid"` for a non-ref-shaped - * `--project-ref`/positional value. Used by both `link` and `config diff`. - */ +/** The branch-name lookup error for an invalid linked ref, shared by `link` and `config diff`. */ export function parentRefInvalidMessage(value: string): string { return `Cannot resolve branch "${value}": the linked project ref is invalid (checked SUPABASE_PROJECT_ID, supabase/.temp/linked-project.json, supabase/.temp/project-ref). Relink the parent project first: supabase link --project-ref `; } diff --git a/apps/cli/src/command-internal/path-match.ts b/apps/cli/src/command-internal/path-match.ts index 58898ffcb1..9258219760 100644 --- a/apps/cli/src/command-internal/path-match.ts +++ b/apps/cli/src/command-internal/path-match.ts @@ -1,11 +1,11 @@ /** - * Matches a glob pattern against a path the way Go's `path.Match` does, for the seed-file - * globber's `[db.seed] sql_paths` expansion. Hand-ported rather than compiled to a `RegExp`, + * Matches a glob pattern against a path using shell-pattern semantics (`*` and `?` never cross + * `/`), for the seed-file globber's `[db.seed] sql_paths` expansion. Hand-ported rather than compiled to a `RegExp`, * since glob character classes diverge from JS regex classes and a malformed pattern must * report `badPattern` instead of being silently reinterpreted. * * Byte-wise (UTF-8), not UTF-16 code units: the `*`-retry loop can land mid-character, whose - * byte decodes as a single invalid rune that a `?` in the retried chunk can then consume. + * byte decodes as a single invalid code point that a `?` in the retried chunk can then consume. */ /** `badPattern` reports a malformed pattern instead of throwing. */ @@ -28,7 +28,7 @@ const UTF8_ENCODER = new TextEncoder(); */ type Bytes = Uint8Array; -const RUNE_ERROR = 0xfffd; +const REPLACEMENT_CHAR = 0xfffd; const SLASH = 0x2f; const STAR = 0x2a; const QUESTION = 0x3f; @@ -38,19 +38,19 @@ const CARET = 0x5e; const HYPHEN = 0x2d; const BACKSLASH = 0x5c; -interface DecodedRune { +interface DecodedCodePoint { readonly r: number; readonly size: number; } /** - * Decodes the UTF-8 rune starting at byte offset `i` of `b`. Any invalid or truncated - * sequence decodes as `(RUNE_ERROR, 1)` — never throws, and never consumes more than the + * Decodes the UTF-8 code point starting at byte offset `i` of `b`. Any invalid or truncated + * sequence decodes as `(REPLACEMENT_CHAR, 1)` — never throws, and never consumes more than the * single invalid lead byte. */ -const decodeRune = (b: Bytes, i: number): DecodedRune => { +const decodeCodePoint = (b: Bytes, i: number): DecodedCodePoint => { const n = b.length - i; - if (n <= 0) return { r: RUNE_ERROR, size: 0 }; + if (n <= 0) return { r: REPLACEMENT_CHAR, size: 0 }; const b0 = b[i]!; if (b0 < 0x80) return { r: b0, size: 1 }; let size: number; @@ -87,17 +87,17 @@ const decodeRune = (b: Bytes, i: number): DecodedRune => { } else { // 0x80-0xC1: a bare continuation byte or an overlong 2-byte lead. 0xF5-0xFF: past // the max valid lead byte. Both are invalid lead bytes. - return { r: RUNE_ERROR, size: 1 }; + return { r: REPLACEMENT_CHAR, size: 1 }; } - if (n < size) return { r: RUNE_ERROR, size: 1 }; + if (n < size) return { r: REPLACEMENT_CHAR, size: 1 }; const b1 = b[i + 1]!; - if (b1 < lo || b1 > hi) return { r: RUNE_ERROR, size: 1 }; + if (b1 < lo || b1 > hi) return { r: REPLACEMENT_CHAR, size: 1 }; if (size === 2) return { r: ((b0 & 0x1f) << 6) | (b1 & 0x3f), size: 2 }; const b2 = b[i + 2]!; - if (b2 < 0x80 || b2 > 0xbf) return { r: RUNE_ERROR, size: 1 }; + if (b2 < 0x80 || b2 > 0xbf) return { r: REPLACEMENT_CHAR, size: 1 }; if (size === 3) return { r: ((b0 & 0x0f) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f), size: 3 }; const b3 = b[i + 3]!; - if (b3 < 0x80 || b3 > 0xbf) return { r: RUNE_ERROR, size: 1 }; + if (b3 < 0x80 || b3 > 0xbf) return { r: REPLACEMENT_CHAR, size: 1 }; return { r: ((b0 & 0x07) << 18) | ((b1 & 0x3f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f), size: 4, @@ -150,9 +150,9 @@ const getEsc = (chunk: Bytes): GetEsc => { c = c.subarray(1); if (c.length === 0) return { r: 0, rest: c, bad: true }; } - const { r, size } = decodeRune(c, 0); + const { r, size } = decodeCodePoint(c, 0); // A genuinely invalid byte, not a literal (valid, 3-byte-encoded) U+FFFD character. - if (r === RUNE_ERROR && size === 1) return { r, rest: c.subarray(1), bad: true }; + if (r === REPLACEMENT_CHAR && size === 1) return { r, rest: c.subarray(1), bad: true }; const rest = c.subarray(size); return { r, rest, bad: rest.length === 0 }; }; @@ -181,7 +181,7 @@ const matchChunk = (chunkIn: Bytes, sIn: Bytes): MatchChunk => { if (op === LBRACKET) { let r = 0; if (!failed) { - const decoded = decodeRune(s, 0); + const decoded = decodeCodePoint(s, 0); r = decoded.r; s = s.subarray(decoded.size); } @@ -215,7 +215,7 @@ const matchChunk = (chunkIn: Bytes, sIn: Bytes): MatchChunk => { } else if (op === QUESTION) { if (!failed) { if (s[0] === SLASH) failed = true; - const { size } = decodeRune(s, 0); + const { size } = decodeCodePoint(s, 0); s = s.subarray(size); } chunk = chunk.subarray(1); @@ -239,8 +239,7 @@ const matchChunk = (chunkIn: Bytes, sIn: Bytes): MatchChunk => { }; /** - * Reports whether `name` matches the shell pattern `pattern`, using Go's `path.Match` - * semantics. `badPattern` is set (instead of throwing) for a malformed pattern. + * Reports whether `name` matches the shell pattern `pattern`. `badPattern` is set (instead of throwing) for a malformed pattern. */ export const pathMatch = (pattern: string, name: string): PathMatchResult => { let pat = UTF8_ENCODER.encode(pattern); diff --git a/apps/cli/src/command-internal/path-match.unit.test.ts b/apps/cli/src/command-internal/path-match.unit.test.ts index 6d0f8ab3d4..d8e60c9d41 100644 --- a/apps/cli/src/command-internal/path-match.unit.test.ts +++ b/apps/cli/src/command-internal/path-match.unit.test.ts @@ -18,7 +18,7 @@ describe("pathMatch", () => { }); }); - describe("`*` and `?` never cross `/` (Go's non-`/` rule)", () => { + describe("`*` and `?` never cross `/`", () => { it.each([ ["*.sql", "sub/seed.sql", false], ["seed?.sql", "seed/.sql", false], @@ -40,14 +40,14 @@ describe("pathMatch", () => { expect(pathMatch(pattern, name).matched).toBe(expected); }); - it("treats a leading `!` as a literal class member, NOT negation (Go parity)", () => { + it("treats a leading `!` as a literal class member, NOT negation", () => { expect(pathMatch("[!a].sql", "!.sql").matched).toBe(true); expect(pathMatch("[!a].sql", "a.sql").matched).toBe(true); expect(pathMatch("[!a].sql", "b.sql").matched).toBe(false); }); }); - describe("byte-offset `*` retry against multibyte characters (Go path.Match parity)", () => { + describe("byte-offset `*` retry against multibyte characters", () => { it.each([ // U+FF01 (fullwidth `!`) is 3 UTF-8 bytes; U+1F600 (😀) is 4 UTF-8 bytes. ["*??.sql", "!.sql", true], @@ -70,7 +70,7 @@ describe("pathMatch", () => { }); }); - describe("malformed patterns report badPattern (Go's path.ErrBadPattern)", () => { + describe("malformed patterns report badPattern", () => { it.each([ "[", // unterminated class "[a", // unterminated class with member diff --git a/apps/cli/src/command-internal/pg-dump.run.ts b/apps/cli/src/command-internal/pg-dump.run.ts index 4a412d32e2..6b248ce3a1 100644 --- a/apps/cli/src/command-internal/pg-dump.run.ts +++ b/apps/cli/src/command-internal/pg-dump.run.ts @@ -7,7 +7,7 @@ type StackRuntimePreference = | { readonly kind: "container"; readonly engine: "docker" | "podman" }; import { NetworkIdFlag } from "./global-flags.ts"; -import { viperEnvStringWithProjectFallback } from "./viper-env.ts"; +import { supabaseEnvStringWithProjectFallback } from "./supabase-env.ts"; import { RuntimeInfo } from "../shared/runtime/runtime-info.service.ts"; import { DockerRun } from "./docker-run.service.ts"; import { DockerRunError } from "./docker-run.errors.ts"; @@ -212,7 +212,7 @@ export const dumpNetworkMode = Effect.fnUntraced(function* ( ): Effect.fn.Return<{ readonly _tag: "named"; readonly name: string } | { readonly _tag: "host" }> { if (networkId !== undefined && networkId.length > 0) return { _tag: "named", name: networkId }; if (forceHostNetwork) return { _tag: "host" }; - const envNetworkId = yield* viperEnvStringWithProjectFallback( + const envNetworkId = yield* supabaseEnvStringWithProjectFallback( "SUPABASE_NETWORK_ID", projectEnvValues, ); diff --git a/apps/cli/src/command-internal/pgdelta.paths.ts b/apps/cli/src/command-internal/pgdelta.paths.ts index 63603fe700..ef3de9c9f4 100644 --- a/apps/cli/src/command-internal/pgdelta.paths.ts +++ b/apps/cli/src/command-internal/pgdelta.paths.ts @@ -2,7 +2,7 @@ * On-disk locations for pg-delta-adjacent cache/snapshot artefacts. * * Two roots: - * - `pgdeltaScratchDir`: `supabase/.temp/pgdelta` in legacy state (Go-shared, workspace-mounted), + * - `pgdeltaScratchDir`: `supabase/.temp/pgdelta` in legacy state (workspace-mounted), * `$SUPABASE_HOME/tmp/pgdelta/` in stack state — catalog snapshots and debug bundles. * - {@link shadowBaselineCacheDir}: global under `SUPABASE_HOME` — the shadow * baseline PGDATA tars, shared across worktrees with the same settings. @@ -10,21 +10,15 @@ import { homedir } from "node:os"; -import type { Path } from "effect"; +import { Effect, type Path } from "effect"; -import { resolveSupabaseHome } from "../shared/config/supabase-home.ts"; +import { readSupabaseHome } from "../shared/config/supabase-home.ts"; /** * Global shadow-baseline cache directory: * `${SUPABASE_HOME}/cache/shadow-baseline` (default `~/.supabase/cache/shadow-baseline`). - * - * Pure: callers may pass `env`/`homeDir` for tests; production uses `process.env` and - * `os.homedir()`. */ -export function shadowBaselineCacheDir( - path: Path.Path, - env: Readonly> = process.env, - homeDir: string = homedir(), -): string { - return path.join(resolveSupabaseHome(path, env, homeDir), "cache", "shadow-baseline"); -} +export const shadowBaselineCacheDir = (path: Path.Path, homeDir: string = homedir()) => + Effect.map(readSupabaseHome(path, homeDir), (home) => + path.join(home, "cache", "shadow-baseline"), + ); diff --git a/apps/cli/src/command-internal/pgdelta.ts b/apps/cli/src/command-internal/pgdelta.ts index b00322df8f..2e8e35090a 100644 --- a/apps/cli/src/command-internal/pgdelta.ts +++ b/apps/cli/src/command-internal/pgdelta.ts @@ -1,6 +1,6 @@ -import { Config, Effect, Option, type Path } from "effect"; +import { Effect } from "effect"; -import { resolveLocalProjectId, sanitizeProjectId } from "./docker-ids.ts"; +import { envValue } from "../shared/config/env-option.ts"; /** * Ambient inputs shared by the pg-delta and migra diff workflows: the project id @@ -20,32 +20,6 @@ export interface PgDeltaContext { readonly projectEnv: Readonly>; } -/** - * Resolves {@link PgDeltaContext.projectId} — not `CommandSettings.projectId` alone, which is - * env-only and resolves to `""` for a project relying on config.toml's `project_id` or the - * workdir-basename default, mounting the wrong `supabase_edge_runtime_` Deno-cache volume. - * Shared by every pg-delta context builder (`db diff`, `db pull`, declarative generate/sync). - * - * When a matched `[remotes.]` block already resolved its own `project_id` into - * `toml.projectId`, the raw `cliProjectId` argument is suppressed entirely — otherwise an - * unrelated ambient `SUPABASE_PROJECT_ID` could win back over the matched remote's id. - */ -export function resolvePgDeltaProjectId( - path: Path.Path, - cliProjectId: Option.Option, - toml: { readonly projectId: Option.Option; readonly appliedRemote: string | undefined }, - workdir: string, -): string { - return sanitizeProjectId( - resolveLocalProjectId( - path, - toml.appliedRemote !== undefined ? undefined : Option.getOrUndefined(cliProjectId), - Option.getOrUndefined(toml.projectId), - workdir, - ), - ); -} - export function isPostgresURL(ref: string): boolean { return ref.startsWith("postgres://") || ref.startsWith("postgresql://"); } @@ -55,11 +29,10 @@ export function edgeRuntimeId(projectId: string): string { } /** Recognizes `PGDELTA_DEBUG=1`/`true`/`yes` (case-insensitive). */ -export const isPgDeltaDebugEnabled = Config.String("PGDELTA_DEBUG").pipe( - Config.withDefault(""), - Config.map((raw) => { - const value = raw.trim().toLowerCase(); +export const isPgDeltaDebugEnabled: Effect.Effect = Effect.map( + envValue("PGDELTA_DEBUG"), + (raw) => { + const value = (raw ?? "").trim().toLowerCase(); return value === "1" || value === "true" || value === "yes"; - }), - Effect.orElseSucceed(() => false), + }, ); diff --git a/apps/cli/src/command-internal/pgdelta.unit.test.ts b/apps/cli/src/command-internal/pgdelta.unit.test.ts index 95fa4e2012..f5aa331800 100644 --- a/apps/cli/src/command-internal/pgdelta.unit.test.ts +++ b/apps/cli/src/command-internal/pgdelta.unit.test.ts @@ -23,7 +23,8 @@ describe("isPgDeltaDebugEnabled", () => { it.effect("is true for 1/true/yes (case-insensitive, trimmed)", () => Effect.gen(function* () { for (const value of ["1", "true", "YES", " True "]) { - expect(yield* withConfigEnv({ PGDELTA_DEBUG: value }, isPgDeltaDebugEnabled)).toBe(true); + const debug = yield* withConfigEnv({ PGDELTA_DEBUG: value }, isPgDeltaDebugEnabled); + expect(debug).toBe(true); } }), ); diff --git a/apps/cli/src/command-internal/pooler-fallback.ts b/apps/cli/src/command-internal/pooler-fallback.ts index 7ecdc66e31..ecc9a0b4d4 100644 --- a/apps/cli/src/command-internal/pooler-fallback.ts +++ b/apps/cli/src/command-internal/pooler-fallback.ts @@ -1,4 +1,4 @@ -import { Effect, Option } from "effect"; +import { Effect, Option, Predicate } from "effect"; import { Output } from "../shared/output/output.service.ts"; import { yellow } from "./colors.ts"; @@ -8,6 +8,21 @@ export function isDirectDbHost(host: string, projectHost: string): boolean { return host.startsWith("db.") && host.endsWith(`.${projectHost}`); } +/** + * Treats a failed pooler resolution as "no fallback" so the caller keeps its original error, except + * an unapproved network-ban removal, whose "re-run with --yes" guidance would otherwise be lost. + */ +export const orNoPoolerFallback = ( + resolve: Effect.Effect, E, R>, +) => + resolve.pipe( + Effect.catch((error) => + Predicate.isTagged(error, "LinkedDbNetworkBannedError") + ? Effect.fail(error) + : Effect.succeed(Option.none()), + ), + ); + export interface PoolerFallbackOptions { readonly run: Effect.Effect; readonly retry: (pooler: PgConnInput) => Effect.Effect; @@ -32,10 +47,12 @@ export const emitPoolerFallbackWarning = (host: string): Effect.Effect( options: PoolerFallbackOptions, -): Effect.Effect { - const resolveFallback = options.resolveFallback.pipe( - Effect.orElseSucceed(() => Option.none()), - ); +): Effect.Effect< + A, + E | (EF & { readonly _tag: "LinkedDbNetworkBannedError" }), + R | R2 | RF | Output +> { + const resolveFallback = orNoPoolerFallback(options.resolveFallback); const retryOrReturn = (result: A) => Effect.gen(function* () { diff --git a/apps/cli/src/command-internal/pooler-fallback.unit.test.ts b/apps/cli/src/command-internal/pooler-fallback.unit.test.ts index b32a0f8444..08e43655ca 100644 --- a/apps/cli/src/command-internal/pooler-fallback.unit.test.ts +++ b/apps/cli/src/command-internal/pooler-fallback.unit.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "@effect/vitest"; import { Effect, Exit, Layer, Option } from "effect"; import { Output } from "../shared/output/output.service.ts"; +import { DbConfigPoolerLoginError, LinkedDbNetworkBannedError } from "./db-config.errors.ts"; import type { PgConnInput } from "./db-connection.service.ts"; import { runWithPoolerFallback } from "./pooler-fallback.ts"; @@ -175,4 +176,46 @@ describe("runWithPoolerFallback", () => { expect(out.stderrText).toContain("Retrying via the IPv4 connection pooler."); }).pipe(Effect.provide(out.layer)); }); + + it.live( + "surfaces an unapproved network-ban removal instead of keeping the original result", + () => { + const out = captureOutput(); + const banned = new LinkedDbNetworkBannedError({ + message: "Network bans (203.0.113.7) may be blocking the connection.", + suggestion: "Re-run with --yes to remove them.", + }); + + return Effect.gen(function* () { + const exit = yield* runWithPoolerFallback({ + run: Effect.succeed({ exitCode: 1, stderr: "network is unreachable" }), + retry: () => Effect.succeed({ exitCode: 0, stderr: "" }), + directHost: "db.abcdefghijklmnopqrst.supabase.co", + eligible: true, + resolveFallback: Effect.fail(banned), + classifyResult: (result: AttemptResult) => result.exitCode !== 0, + }).pipe(Effect.exit); + + expect(exit).toEqual(Exit.fail(banned)); + expect(out.stderrText).toBe(""); + }).pipe(Effect.provide(out.layer)); + }, + ); + + it.live("keeps the original result when the pooler cannot be resolved for another reason", () => { + const out = captureOutput(); + + return Effect.gen(function* () { + const result = yield* runWithPoolerFallback({ + run: Effect.succeed({ exitCode: 1, stderr: "network is unreachable" }), + retry: () => Effect.succeed({ exitCode: 0, stderr: "" }), + directHost: "db.abcdefghijklmnopqrst.supabase.co", + eligible: true, + resolveFallback: Effect.fail(new DbConfigPoolerLoginError({ message: "tenant mismatch" })), + classifyResult: (result: AttemptResult) => result.exitCode !== 0, + }); + + expect(result).toEqual({ exitCode: 1, stderr: "network is unreachable" }); + }).pipe(Effect.provide(out.layer)); + }); }); diff --git a/apps/cli/src/command-internal/postgres-url.ts b/apps/cli/src/command-internal/postgres-url.ts index 02aa1b22ef..bf72a4c761 100644 --- a/apps/cli/src/command-internal/postgres-url.ts +++ b/apps/cli/src/command-internal/postgres-url.ts @@ -13,7 +13,7 @@ function isIPv6Host(host: string): boolean { * and turns space into `+` (`application/x-www-form-urlencoded` query-escaping, not * `encodeURIComponent`, which differs on space and `!*'()`). */ -function goQueryEscape(value: string): string { +function queryEscape(value: string): string { let out = ""; for (const ch of value) { if (/[A-Za-z0-9\-_.~]/.test(ch)) { @@ -74,7 +74,7 @@ export function toPostgresURL(conn: PostgresUrlInput): string { // tenant on pooler fallback. const optionsParam = conn.options !== undefined && conn.options.length > 0 - ? `&options=${goQueryEscape(conn.options)}` + ? `&options=${queryEscape(conn.options)}` : ""; // Every other runtime param (search_path, statement_timeout, …), sorted for stable // serialization. @@ -83,7 +83,7 @@ export function toPostgresURL(conn: PostgresUrlInput): string { ? "" : Object.keys(conn.runtimeParams) .sort() - .map((key) => `&${goQueryEscape(key)}=${goQueryEscape(conn.runtimeParams![key]!)}`) + .map((key) => `&${queryEscape(key)}=${queryEscape(conn.runtimeParams![key]!)}`) .join(""); return `postgresql://${userinfo}@${host}:${conn.port}/${encodeURIComponent( conn.database, diff --git a/apps/cli/src/command-internal/postgres-url.unit.test.ts b/apps/cli/src/command-internal/postgres-url.unit.test.ts index 2088bdad7c..a8a948bbca 100644 --- a/apps/cli/src/command-internal/postgres-url.unit.test.ts +++ b/apps/cli/src/command-internal/postgres-url.unit.test.ts @@ -47,14 +47,14 @@ describe("toPostgresURL", () => { }); it("appends the pooler `options` runtime param after connect_timeout", () => { - // Go's ToPostgresURL appends RuntimeParams; the Supavisor tenant routing + // The Supavisor tenant routing // `options=reference=` must reach pg-delta (`=` escaped to %3D). expect(toPostgresURL({ ...base, options: "reference=abcdefghijklmnop" })).toBe( "postgresql://postgres:postgres@127.0.0.1:54322/postgres?connect_timeout=10&options=reference%3Dabcdefghijklmnop", ); }); - it("matches Go's url.QueryEscape for options (space → +)", () => { + it("query-escapes options (space → +)", () => { expect(toPostgresURL({ ...base, options: "-c search_path=public" })).toContain( "&options=-c+search_path%3Dpublic", ); @@ -67,7 +67,7 @@ describe("toPostgresURL", () => { ); }); - it("appends every runtimeParams entry (sorted) after options, like Go ToPostgresURL", () => { + it("appends every runtimeParams entry (sorted) after options", () => { expect( toPostgresURL({ ...base, @@ -79,7 +79,7 @@ describe("toPostgresURL", () => { ); }); - it("escapes runtimeParams values like Go's url.QueryEscape", () => { + it("query-escapes runtimeParams values", () => { expect(toPostgresURL({ ...base, runtimeParams: { search_path: "a b,c" } })).toContain( "&search_path=a+b%2Cc", ); diff --git a/apps/cli/src/command-internal/profile-load.ts b/apps/cli/src/command-internal/profile-load.ts index 221c79493b..940bf91c9b 100644 --- a/apps/cli/src/command-internal/profile-load.ts +++ b/apps/cli/src/command-internal/profile-load.ts @@ -64,17 +64,17 @@ export function loadProfile( }; } - // An empty token falls back to the established "no config file" search-path error. + // An empty token has no config file to read. if (token === "") { - return yield* failRead(`Config File "config" Not Found in "[]"`); + return yield* failRead("no profile config file specified"); } - const ext = goFilepathExt(token); - if (!VIPER_SUPPORTED_EXTS.has(ext)) { + const ext = fileExtension(token); + if (!PROFILE_FILE_EXTS.has(ext)) { const quoted = yield* quoteJsonString(ext).pipe( Effect.mapError((error) => readError(error.message)), ); - return yield* failRead(`Unsupported Config Type ${quoted}`); + return yield* failRead(`unsupported config file type ${quoted}`); } const content = yield* fs @@ -95,17 +95,13 @@ export function loadProfile( let parsed = yield* Effect.try({ try: (): unknown => parseYaml(content), - catch: (cause) => readError(`While parsing config: ${parseDetail(cause)}`), + catch: (cause) => readError(`invalid config file: ${parseDetail(cause)}`), }); if (parsed === null || parsed === undefined) { parsed = {}; } if (typeof parsed !== "object" || Array.isArray(parsed)) { - // A non-mapping YAML document (e.g. a scalar or list) is rejected; detail text is - // best-effort. - return yield* failRead( - `While parsing config: yaml: unmarshal errors:\n cannot unmarshal into map[string]interface {}`, - ); + return yield* failRead("invalid config file: expected a YAML mapping at the top level"); } // Configuration keys are lowercased before decoding, so `API_URL:`/`Name:` behave like // their lowercase spellings, and unknown-key errors report the lowercased form. A @@ -115,15 +111,15 @@ export function loadProfile( config[key.toLowerCase()] = value; } - // Unknown keys abort decoding, reported sorted in a padded multi-line block. + // Unknown keys abort decoding, reported sorted. const invalidKeys = Object.keys(config) .filter((key) => !PROFILE_STRUCT_KEYS.has(key)) .sort(); if (invalidKeys.length > 0) { - return yield* failDecode(`'utils.Profile' has invalid keys: ${invalidKeys.join(", ")}`); + return yield* failDecode(`unknown keys: ${invalidKeys.join(", ")}`); } - // Weak scalar decoding + per-field tag validation, in struct field order. + // Weak scalar decoding + per-field format validation, in field order. const decodeErrors: string[] = []; const validationErrors: string[] = []; const values = new Map(); @@ -131,27 +127,28 @@ export function loadProfile( const raw = config[field.key]; const weak = weakString(raw); if (weak === undefined) { - decodeErrors.push( - `'${field.goName}' expected type 'string', got unconvertible type '${goTypeName(raw)}', value: '${goValueString(raw)}'`, + const shown = yield* encodeJsonValue(raw).pipe( + Effect.mapError((error) => readError(error.message)), ); + decodeErrors.push(`${field.key}: expected a string, got ${valueTypeName(raw)}: ${shown}`); continue; } values.set(field.key, weak); if (weak === "") { if (field.required) { - validationErrors.push(validatorLine(field.goName, "required")); + validationErrors.push(`${field.key} is required`); } continue; } if (field.format !== undefined && !FORMAT_TAG_CHECKS[field.format](weak)) { - validationErrors.push(validatorLine(field.goName, field.format)); + validationErrors.push(`${field.key} ${FORMAT_TAG_DESCRIPTIONS[field.format]}`); } } if (decodeErrors.length > 0) { return yield* failDecode(decodeErrors.join("\n")); } if (validationErrors.length > 0) { - return yield* fail(padGoErrorBlock(`invalid profile: ${validationErrors.join("\n")}`)); + return yield* fail(`invalid profile:\n${validationErrors.join("\n")}`); } // All required fields passed validation above; pooler_host stays "" when absent. @@ -174,16 +171,14 @@ const failRead = (detail: string) => Effect.fail(readError(detail)); const quoteJsonString = Schema.encodeEffect(Schema.fromJsonString(Schema.String)); +/** JSON rendering of a non-string profile value for decode-error messages. */ +const encodeJsonValue = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); + /** Aggregate decode-error template: multiple failing fields render as one block. */ -const failDecode = (detail: string) => - fail( - padGoErrorBlock( - `failed to parse profile: decoding failed due to the following error(s):\n\n${detail}`, - ), - ); +const failDecode = (detail: string) => fail(`failed to parse profile:\n${detail}`); /** Recognized config file extensions, checked case-sensitively. */ -const VIPER_SUPPORTED_EXTS: ReadonlySet = new Set([ +const PROFILE_FILE_EXTS: ReadonlySet = new Set([ "json", "toml", "yaml", @@ -202,7 +197,7 @@ const VIPER_SUPPORTED_EXTS: ReadonlySet = new Set([ * Returns everything after the last `.` in the final path segment, including for dot-files * (`.yml` → `yml`), where Node's `path.extname` returns `""`. */ -function goFilepathExt(token: string): string { +function fileExtension(token: string): string { for (let i = token.length - 1; i >= 0 && token[i] !== "/"; i--) { if (token[i] === ".") { return token.slice(i + 1); @@ -228,7 +223,6 @@ type FormatTag = "http_url" | "hostname_rfc1123" | "uuid4"; interface ProfileStringField { readonly key: string; - readonly goName: string; readonly required: boolean; readonly format?: FormatTag; } @@ -238,19 +232,29 @@ interface ProfileStringField { * `regions` (a slice) is exempt from weak string decoding and never validated. */ const PROFILE_STRING_FIELDS: ReadonlyArray = [ - { key: "name", goName: "Name", required: true }, - { key: "api_url", goName: "APIURL", required: true, format: "http_url" }, - { key: "dashboard_url", goName: "DashboardURL", required: true, format: "http_url" }, - { key: "docs_url", goName: "DocsURL", required: false, format: "http_url" }, - { key: "project_host", goName: "ProjectHost", required: true, format: "hostname_rfc1123" }, - { key: "pooler_host", goName: "PoolerHost", required: false, format: "hostname_rfc1123" }, - { key: "client_id", goName: "AuthClientID", required: false, format: "uuid4" }, - { key: "studio_image", goName: "StudioImage", required: false }, + { key: "name", required: true }, + { key: "api_url", required: true, format: "http_url" }, + { key: "dashboard_url", required: true, format: "http_url" }, + { key: "docs_url", required: false, format: "http_url" }, + { + key: "project_host", + required: true, + format: "hostname_rfc1123", + }, + { + key: "pooler_host", + required: false, + format: "hostname_rfc1123", + }, + { key: "client_id", required: false, format: "uuid4" }, + { key: "studio_image", required: false }, ]; -function validatorLine(goName: string, tag: string): string { - return `Key: 'Profile.${goName}' Error:Field validation for '${goName}' failed on the '${tag}' tag`; -} +const FORMAT_TAG_DESCRIPTIONS: Record = { + http_url: "must be an http(s) URL", + hostname_rfc1123: "must be a valid hostname", + uuid4: "must be a lowercase UUID v4", +}; /** RFC 1123 hostname pattern. */ const HOSTNAME_RFC1123 = /^([a-zA-Z0-9][a-zA-Z0-9-]{0,62})(\.[a-zA-Z0-9][a-zA-Z0-9-]{0,62})*?$/; @@ -274,7 +278,7 @@ const FORMAT_TAG_CHECKS: Record boolean> = { /** * Weak string coercion: strings pass through, booleans become `"1"`/`"0"`, numbers are - * stringified, and `null`/`undefined` decode to `""`. Arrays/objects are unconvertible + * stringified, and `null`/`undefined` decode to `""`. Arrays/objects are unsupported * (`undefined`, a decode error). */ function weakString(value: unknown): string | undefined { @@ -285,34 +289,12 @@ function weakString(value: unknown): string | undefined { return undefined; } -function goTypeName(value: unknown): string { - if (Array.isArray(value)) return "[]interface {}"; - if (typeof value === "object" && value !== null) return "map[string]interface {}"; +function valueTypeName(value: unknown): string { + if (Array.isArray(value)) return "array"; + if (typeof value === "object" && value !== null) return "object"; return typeof value; } -/** Best-effort rendering of an invalid field's value for the error message. */ -function goValueString(value: unknown): string { - if (Array.isArray(value)) return `[${value.map(goValueString).join(" ")}]`; - if (typeof value === "object" && value !== null) { - const entries = Object.entries(value as Record) - .map(([key, entry]) => `${key}:${goValueString(entry)}`) - .join(" "); - return `map[${entries}]`; - } - return String(value); -} - -/** - * Pads every line (including blank ones) with trailing spaces to the longest line's width, to - * match the CLI's established multi-line error rendering exactly. - */ -export function padGoErrorBlock(message: string): string { - const lines = message.split("\n"); - const width = Math.max(...lines.map((line) => line.length)); - return lines.map((line) => line.padEnd(width)).join("\n"); -} - function parseDetail(cause: unknown): string { return cause instanceof Error ? cause.message : String(cause); } diff --git a/apps/cli/src/command-internal/profile-load.unit.test.ts b/apps/cli/src/command-internal/profile-load.unit.test.ts index ead3c719b4..8543efc1f3 100644 --- a/apps/cli/src/command-internal/profile-load.unit.test.ts +++ b/apps/cli/src/command-internal/profile-load.unit.test.ts @@ -2,7 +2,7 @@ import { BunServices } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; import { Effect, FileSystem, Path } from "effect"; -import { loadProfile, padGoErrorBlock, type ProfileLoadError } from "./profile-load.ts"; +import { loadProfile, type ProfileLoadError } from "./profile-load.ts"; const load = (token: string) => Effect.gen(function* () { @@ -33,7 +33,7 @@ const writeProfile = (name: string, content: string) => }).pipe(Effect.provide(BunServices.layer)); describe("loadProfile", () => { - it.effect("resolves built-in profile names case-insensitively (Go strings.EqualFold)", () => + it.effect("resolves built-in profile names case-insensitively", () => Effect.gen(function* () { expect(yield* load("SUPABASE-LOCAL")).toBe("http://localhost:8080"); expect(yield* load("supabase")).toBe("https://api.supabase.com"); @@ -42,11 +42,9 @@ describe("loadProfile", () => { }), ); - it.effect("fails on an empty token with viper's search-mode error (Go `--profile=`)", () => + it.effect("fails an empty --profile= token with the search-mode error", () => Effect.gen(function* () { - expect(yield* loadError("")).toBe( - `failed to read profile: Config File "config" Not Found in "[]"`, - ); + expect(yield* loadError("")).toBe(`failed to read profile: no profile config file specified`); }), ); @@ -54,15 +52,15 @@ describe("loadProfile", () => { Effect.gen(function* () { // --profile --metadata-url … expect(yield* loadError("--metadata-url")).toBe( - `failed to read profile: Unsupported Config Type ""`, + `failed to read profile: unsupported config file type ""`, ); expect(yield* loadError("profile.txt")).toBe( - `failed to read profile: Unsupported Config Type "txt"`, + `failed to read profile: unsupported config file type "txt"`, ); }), ); - it.effect("uses Go filepath.Ext semantics for dot-files (`.yml` IS extension `yml`)", () => + it.effect("treats dot-files as having an extension (`.yml` IS extension `yml`)", () => Effect.gen(function* () { const missing = yield* tempPath(".yml"); expect(yield* loadError(missing)).toBe( @@ -71,7 +69,7 @@ describe("loadProfile", () => { }).pipe(Effect.provide(BunServices.layer)), ); - it.effect("fails on a missing file with Go's os.Open error", () => + it.effect("fails on a missing file with the open error", () => Effect.gen(function* () { expect(yield* loadError("missing.yml")).toBe( `failed to read profile: open missing.yml: no such file or directory`, @@ -79,7 +77,7 @@ describe("loadProfile", () => { }), ); - it.effect("fails on a directory with Go's read error", () => + it.effect("fails on a directory with the read error", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const dir = yield* tempPath("dir.yml"); @@ -103,7 +101,7 @@ describe("loadProfile", () => { }), ); - it.effect("accepts mixed-case keys like viper's insensitive decode (probed on go1.26)", () => + it.effect("accepts mixed-case keys case-insensitively", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const file = yield* writeProfile( @@ -146,7 +144,7 @@ describe("loadProfile", () => { }).pipe(Effect.provide(BunServices.layer)), ); - it.effect("reports unknown keys LOWERCASED, like viper's pre-decode normalization", () => + it.effect("reports unknown keys LOWERCASED", () => Effect.gen(function* () { const file = yield* writeProfile( "bogus-upper.yml", @@ -158,30 +156,28 @@ describe("loadProfile", () => { "BOGUS_KEY: x", ].join("\n"), ); - expect(yield* loadError(file)).toContain("'utils.Profile' has invalid keys: bogus_key"); + expect(yield* loadError(file)).toContain("unknown keys: bogus_key"); }), ); - it.effect( - "returns Go's CurrentProfile.Name — the canonical built-in or the file's name field", - () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - expect((yield* loadProfile("SUPABASE-LOCAL", fs)).name).toBe("supabase-local"); - const file = yield* writeProfile( - "named.yml", - [ - "name: harness", - "api_url: http://127.0.0.1:44444", - "dashboard_url: http://127.0.0.1:44444/dashboard", - "project_host: supabase.co", - ].join("\n"), - ); - expect((yield* loadProfile(file, fs)).name).toBe("harness"); - }).pipe(Effect.provide(BunServices.layer)), + it.effect("returns the profile name — the canonical built-in or the file's name field", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + expect((yield* loadProfile("SUPABASE-LOCAL", fs)).name).toBe("supabase-local"); + const file = yield* writeProfile( + "named.yml", + [ + "name: harness", + "api_url: http://127.0.0.1:44444", + "dashboard_url: http://127.0.0.1:44444/dashboard", + "project_host: supabase.co", + ].join("\n"), + ); + expect((yield* loadProfile(file, fs)).name).toBe("harness"); + }).pipe(Effect.provide(BunServices.layer)), ); - it.effect("rejects unknown keys with mapstructure's padded UnmarshalExact block", () => + it.effect("rejects unknown keys with a sorted unknown-keys line", () => Effect.gen(function* () { const file = yield* writeProfile( "extra-keys.yml", @@ -194,27 +190,24 @@ describe("loadProfile", () => { "db_url: postgres://localhost:5432/db", ].join("\n"), ); - const line1 = "failed to parse profile: decoding failed due to the following error(s):"; - const line3 = "'utils.Profile' has invalid keys: db_url, gotrue_url"; expect(yield* loadError(file)).toBe( - [line1, "".padEnd(line1.length), line3.padEnd(line1.length)].join("\n"), + "failed to parse profile:\nunknown keys: db_url, gotrue_url", ); }), ); - it.effect( - "reports missing required fields with the validator's padded lines, in struct order", - () => - Effect.gen(function* () { - const file = yield* writeProfile("incomplete.yml", "name: incomplete\n"); - const lines = [ - "invalid profile: Key: 'Profile.APIURL' Error:Field validation for 'APIURL' failed on the 'required' tag", - "Key: 'Profile.DashboardURL' Error:Field validation for 'DashboardURL' failed on the 'required' tag", - "Key: 'Profile.ProjectHost' Error:Field validation for 'ProjectHost' failed on the 'required' tag", - ]; - const width = Math.max(...lines.map((line) => line.length)); - expect(yield* loadError(file)).toBe(lines.map((line) => line.padEnd(width)).join("\n")); - }), + it.effect("reports missing required fields one per line, in field order", () => + Effect.gen(function* () { + const file = yield* writeProfile("incomplete.yml", "name: incomplete\n"); + expect(yield* loadError(file)).toBe( + [ + "invalid profile:", + "api_url is required", + "dashboard_url is required", + "project_host is required", + ].join("\n"), + ); + }), ); it.effect("reports a missing name (only) — required covers empty strings", () => @@ -227,29 +220,23 @@ describe("loadProfile", () => { "project_host: supabase.co", ].join("\n"), ); - expect(yield* loadError(file)).toBe( - "invalid profile: Key: 'Profile.Name' Error:Field validation for 'Name' failed on the 'required' tag", - ); + expect(yield* loadError(file)).toBe("invalid profile:\nname is required"); }), ); - it.effect( - "weakly stringifies scalars like viper, so `api_url: 123` fails http_url, not decoding", - () => - Effect.gen(function* () { - const file = yield* writeProfile( - "typebad.yml", - [ - "name: t", - "api_url: 123", - "dashboard_url: http://127.0.0.1:44444/dashboard", - "project_host: supabase.co", - ].join("\n"), - ); - expect(yield* loadError(file)).toBe( - "invalid profile: Key: 'Profile.APIURL' Error:Field validation for 'APIURL' failed on the 'http_url' tag", - ); - }), + it.effect("weakly stringifies scalars, so `api_url: 123` fails http_url, not decoding", () => + Effect.gen(function* () { + const file = yield* writeProfile( + "typebad.yml", + [ + "name: t", + "api_url: 123", + "dashboard_url: http://127.0.0.1:44444/dashboard", + "project_host: supabase.co", + ].join("\n"), + ); + expect(yield* loadError(file)).toBe("invalid profile:\napi_url must be an http(s) URL"); + }), ); it.effect("validates the hostname_rfc1123 and http_url format tags", () => @@ -263,24 +250,25 @@ describe("loadProfile", () => { "project_host: 'bad host!'", ].join("\n"), ); - const lines = [ - "invalid profile: Key: 'Profile.APIURL' Error:Field validation for 'APIURL' failed on the 'http_url' tag", - "Key: 'Profile.ProjectHost' Error:Field validation for 'ProjectHost' failed on the 'hostname_rfc1123' tag", - ]; - const width = Math.max(...lines.map((line) => line.length)); - expect(yield* loadError(file)).toBe(lines.map((line) => line.padEnd(width)).join("\n")); + expect(yield* loadError(file)).toBe( + [ + "invalid profile:", + "api_url must be an http(s) URL", + "project_host must be a valid hostname", + ].join("\n"), + ); }), ); - it.effect("fails a malformed YAML file closed with viper's parse prefix", () => + it.effect("fails a malformed YAML file closed with the parse-error prefix", () => Effect.gen(function* () { const file = yield* writeProfile("malformed.yml", "name: [broken\n api_url"); const message = yield* loadError(file); - expect(message).toMatch(/^failed to read profile: While parsing config: /); + expect(message).toMatch(/^failed to read profile: invalid config file: /); }), ); - it.effect("fails closed on unconvertible values (array on a string field)", () => + it.effect("fails closed on non-scalar values (array on a string field)", () => Effect.gen(function* () { const file = yield* writeProfile( "arrayval.yml", @@ -292,22 +280,9 @@ describe("loadProfile", () => { ].join("\n"), ); const message = yield* loadError(file); - expect(message).toContain( - "failed to parse profile: decoding failed due to the following error(s):", - ); - expect(message).toContain( - "'APIURL' expected type 'string', got unconvertible type '[]interface {}'", + expect(message).toBe( + 'failed to parse profile:\napi_url: expected a string, got array: ["http://a","http://b"]', ); }), ); }); - -describe("padGoErrorBlock", () => { - it("pads every line — including blank ones — to the longest line's width", () => { - expect(padGoErrorBlock("abc\n\nlonger line")).toBe("abc \n \nlonger line"); - }); - - it("leaves single-line messages untouched", () => { - expect(padGoErrorBlock("only line")).toBe("only line"); - }); -}); diff --git a/apps/cli/src/command-internal/profile.unit.test.ts b/apps/cli/src/command-internal/profile.unit.test.ts index a83ae15b7f..7bf618d3be 100644 --- a/apps/cli/src/command-internal/profile.unit.test.ts +++ b/apps/cli/src/command-internal/profile.unit.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest"; import { billingUrl, dashboardUrl, poolerHost, projectHost } from "./profile.ts"; describe("projectHost", () => { - it("maps built-in profile names to the Go project_host", () => { + it("maps built-in profile names to the project_host", () => { expect(projectHost("supabase")).toBe("supabase.co"); expect(projectHost("supabase-staging")).toBe("supabase.red"); expect(projectHost("supabase-local")).toBe("supabase.red"); @@ -16,7 +16,7 @@ describe("projectHost", () => { }); describe("poolerHost", () => { - it("maps built-in profile names to the Go pooler_host", () => { + it("maps built-in profile names to the pooler_host", () => { expect(poolerHost("supabase")).toBe("supabase.com"); expect(poolerHost("supabase-staging")).toBe("supabase.green"); expect(poolerHost("snap")).toBe("snapcloud.co"); @@ -32,7 +32,7 @@ describe("poolerHost", () => { }); describe("dashboardUrl", () => { - it("maps built-in profile names to the Go dashboard_url", () => { + it("maps built-in profile names to the dashboard_url", () => { expect(dashboardUrl("supabase")).toBe("https://supabase.com/dashboard"); expect(dashboardUrl("supabase-staging")).toBe("https://supabase.green/dashboard"); expect(dashboardUrl("supabase-local")).toBe("http://localhost:8082"); diff --git a/apps/cli/src/command-internal/project-create-core.ts b/apps/cli/src/command-internal/project-create-core.ts index 4735453730..98322ce4fe 100644 --- a/apps/cli/src/command-internal/project-create-core.ts +++ b/apps/cli/src/command-internal/project-create-core.ts @@ -4,9 +4,16 @@ import { Effect, Option } from "effect"; import { CommandPlatformApi } from "../auth/command-platform-api.service.ts"; import { CommandSettings } from "../config/command-settings.service.ts"; import { OutputFlag } from "./global-flags.ts"; +import { requireHostedMutationConsent } from "./hosted-consent/hosted-consent.ts"; +import type { HostedMutationConsent } from "./hosted-consent/hosted-consent.token.ts"; import { Output } from "../shared/output/output.service.ts"; -import { encodeEnv, encodeGoJson } from "./go-output.encoders.ts"; -import { encodeGoToml, encodeGoYaml, goString, goStruct } from "./go-struct-output.encoders.ts"; +import { encodeEnv, encodeSortedJson } from "./output.encoders.ts"; +import { + encodeStructToml, + encodeStructYaml, + shapeString, + shapeStruct, +} from "./struct-output.encoders.ts"; import { sanitizeErrorBody } from "./http-errors.ts"; import { ProjectsCreateNetworkError, @@ -27,15 +34,15 @@ import { type CreateInput = typeof V1CreateAProjectInput.Type; /** Struct spec driving `-o yaml|toml` key casing for `projects create`'s raw response. */ -const GO_PROJECT_RESPONSE = goStruct([ - ["created_at", goString], - ["id", goString], - ["name", goString], - ["organization_id", goString], - ["organization_slug", goString], - ["ref", goString], - ["region", goString], - ["status", goString], +const PROJECT_RESPONSE_SHAPE = shapeStruct([ + ["created_at", shapeString], + ["id", shapeString], + ["name", shapeString], + ["organization_id", shapeString], + ["organization_slug", shapeString], + ["ref", shapeString], + ["region", shapeString], + ["status", shapeString], ]); export interface ProjectCreateInput { @@ -53,6 +60,8 @@ export interface ProjectCreateInput { * `bootstrap` suppresses it since it emits its own top-level result instead. */ readonly emitStructuredResult: boolean; + /** Proof the caller already confirmed the create; without it the core asks before the POST. */ + readonly consent?: HostedMutationConsent; } /** Formats `key: value`, padding `key:` to width 20. */ @@ -72,7 +81,7 @@ export const projectCreateCore = Effect.fn("ProjectCreate.run")(function* ( input: ProjectCreateInput, ) { const output = yield* Output; - const goOutputFlag = yield* OutputFlag; + const outputFlag = yield* OutputFlag; const api = yield* CommandPlatformApi; const cliSettings = yield* CommandSettings; @@ -120,6 +129,12 @@ export const projectCreateCore = Effect.fn("ProjectCreate.run")(function* ( ...(input.templateUrl !== undefined ? { template_url: input.templateUrl } : {}), }; + if (input.consent === undefined) { + yield* requireHostedMutationConsent({ + action: `Create project "${name}" in organization ${orgId}`, + }); + } + const creating = output.format === "text" ? yield* output.task("Creating project...") : undefined; // `executeRaw` skips output decoding so the created project is emitted as the API returned it, @@ -158,20 +173,20 @@ export const projectCreateCore = Effect.fn("ProjectCreate.run")(function* ( const projectUrl = `${dashboardUrlForProfile(cliSettings.profile)}/project/${id}`; yield* output.raw(`Created a new project at ${projectUrl}\n`, "stderr"); - const goFmt = Option.getOrUndefined(goOutputFlag); - if (goFmt === "json") { - yield* output.raw(encodeGoJson(created)); + const outputFlagFormat = Option.getOrUndefined(outputFlag); + if (outputFlagFormat === "json") { + yield* output.raw(encodeSortedJson(created)); return result; } - if (goFmt === "yaml") { - yield* output.raw(encodeGoYaml(created, GO_PROJECT_RESPONSE)); + if (outputFlagFormat === "yaml") { + yield* output.raw(encodeStructYaml(created, PROJECT_RESPONSE_SHAPE)); return result; } - if (goFmt === "toml") { - yield* output.raw(encodeGoToml(created, GO_PROJECT_RESPONSE)); + if (outputFlagFormat === "toml") { + yield* output.raw(encodeStructToml(created, PROJECT_RESPONSE_SHAPE)); return result; } - if (goFmt === "env") { + if (outputFlagFormat === "env") { yield* output.raw(encodeEnv(created) + "\n"); return result; } diff --git a/apps/cli/src/command-internal/project-environment.ts b/apps/cli/src/command-internal/project-environment.ts deleted file mode 100644 index ebccf2a310..0000000000 --- a/apps/cli/src/command-internal/project-environment.ts +++ /dev/null @@ -1,132 +0,0 @@ -import type { CliProjectEnvironment } from "@supabase/config"; -import { Config, Data, Effect, FileSystem, Option, Path } from "effect"; - -import { - actionability, - type CliErrorActionabilityDeclaration, - ErrorActionabilityFingerprintId, - ErrorActionabilityId, -} from "../shared/telemetry/error-actionability.ts"; -import { parseDotEnv } from "./dotenv.ts"; - -/** - * Dotenv filenames to check, in precedence order, for a given `SUPABASE_ENV` value - * (`.env..local`, `.env.local` — skipped for `"test"` — `.env.`, `.env`). - * - * Fills a gap in `@supabase/config`'s `loadCliProjectEnvironment`, which only resolves this - * order for the `supabase/` directory: this module also checks the project root, and applies - * `SUPABASE_ENV`-selected filenames there too. Its own resolved values still take precedence - * over anything found here, since it's already correct for the keys it knows about. - */ -export function candidateDotenvFilenames(env: string): ReadonlyArray { - return [`.env.${env}.local`, ...(env === "test" ? [] : [".env.local"]), `.env.${env}`, ".env"]; -} - -export class ProjectEnvironmentError extends Data.Error<{ readonly message: string }> { - static readonly [ErrorActionabilityFingerprintId] = "ProjectEnvironmentError"; - get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { - return actionability.invalidConfig; - } -} - -/** - * Reads and parses a dotenv file, or `undefined` if it doesn't exist. Delegates to - * {@link parseDotEnv} rather than a hand-rolled line scanner, so a quoted value spanning - * physical lines (a PEM/private key) parses correctly. - * - * Fails on a malformed line (not blank, a comment, or a `KEY=VALUE`/`KEY: VALUE` - * assignment) — the caller must fail rather than silently skip it. - */ -const readDotEnvFile = Effect.fnUntraced(function* (path: string) { - const fs = yield* FileSystem.FileSystem; - if (!(yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false)))) return undefined; - - const bytes = yield* fs.readFile(path).pipe( - Effect.mapError( - ({ reason }) => - new ProjectEnvironmentError({ - message: reason.cause instanceof Error ? reason.cause.message : reason.message, - }), - ), - ); - const contents = new TextDecoder("utf-8", { ignoreBOM: true }).decode(bytes); - return yield* Effect.try({ - try: () => parseDotEnv(contents), - catch: (cause) => - new ProjectEnvironmentError({ - message: `failed to parse environment file: ${path} (${cause instanceof Error ? cause.message : String(cause)})`, - }), - }); -}); - -/** - * Merged env-var map for `stop`/`status` to read `SUPABASE_*` overrides from, covering the - * project-root and `SUPABASE_ENV`-selected files {@link candidateDotenvFilenames} adds beyond - * `loadCliProjectEnvironment`. - * - * Only `projectEnv`'s ambient-sourced entries are layered back on top: its other, file-derived - * entries aren't necessarily higher-precedence than the env-specific files resolved here, and - * it has no notion of those filenames to tell the two apart itself. - * - * `projectEnv` is `null` when no config file was found, but dotenv loading isn't gated on - * that — fall back to deriving `/supabase` and `workdir` directly, with `process.env` - * as the ambient layer. - */ -export const resolveProjectEnvironmentValues = Effect.fn("ProjectEnvironment.resolve")(function* ( - projectEnv: CliProjectEnvironment | null, - workdir: string, - supabaseEnv?: string, -) { - const path = yield* Path.Path; - const env = - supabaseEnv || - Option.getOrUndefined( - yield* Config.option(Config.String("SUPABASE_ENV")).pipe( - Effect.mapError( - () => - new ProjectEnvironmentError({ - message: "failed to resolve environment variable: SUPABASE_ENV", - }), - ), - ), - ) || - "development"; - const filenames = [...new Set(candidateDotenvFilenames(env))]; - const merged: Record = {}; - let fileCount = 0; - - const supabaseDir = projectEnv?.paths.supabaseDir ?? path.join(workdir, "supabase"); - const projectRoot = projectEnv?.paths.projectRoot ?? workdir; - - // supabase/ dir first, then its parent (the project root). Within a directory, - // "never override an already-set var" means first-processed-wins, so skipping keys - // already present reproduces both orderings at once. - for (const dir of [supabaseDir, projectRoot]) { - for (const filename of filenames) { - const parsed = yield* readDotEnvFile(path.join(dir, filename)); - if (parsed === undefined) continue; - fileCount += 1; - for (const [key, value] of Object.entries(parsed)) { - if (!(key in merged)) merged[key] = value; - } - } - } - yield* Effect.annotateCurrentSpan("file.count", fileCount); - - const ambientOverrides: Record = {}; - if (projectEnv !== null) { - for (const [key, value] of Object.entries(projectEnv.values)) { - if (projectEnv.sources[key] === "ambient") { - ambientOverrides[key] = value; - } - } - } else { - for (const [key, value] of Object.entries(process.env)) { - if (value !== undefined) { - ambientOverrides[key] = value; - } - } - } - - return { ...merged, ...ambientOverrides }; -}); diff --git a/apps/cli/src/command-internal/project-environment.unit.test.ts b/apps/cli/src/command-internal/project-environment.unit.test.ts deleted file mode 100644 index 51e9158099..0000000000 --- a/apps/cli/src/command-internal/project-environment.unit.test.ts +++ /dev/null @@ -1,610 +0,0 @@ -import { BunServices } from "@effect/platform-bun"; -import { describe, expect, layer } from "@effect/vitest"; -import type { CliProjectEnvironment } from "@supabase/config"; -import { ConfigProvider, Effect, FileSystem, Path, Tracer } from "effect"; - -import { withConfigEnv, withEnvVar } from "../../tests/helpers/command-mocks.ts"; -import { ProjectEnvironmentError, resolveProjectEnvironmentValues } from "./project-environment.ts"; - -const project = Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const root = yield* fs.makeTempDirectoryScoped({ prefix: "supabase-project-env-" }); - const supabaseDir = path.join(root, "supabase"); - yield* fs.makeDirectory(supabaseDir, { recursive: true }); - return { fs, path, root, supabaseDir }; -}); - -function fakeProjectEnv( - { path, root, supabaseDir }: Effect.Success, - values: Record = {}, - sources: Record = {}, -): CliProjectEnvironment { - return { - paths: { - projectRoot: root, - supabaseDir, - configPath: path.join(supabaseDir, "config.toml"), - envPath: path.join(supabaseDir, ".env"), - envLocalPath: path.join(supabaseDir, ".env.local"), - }, - values, - loadedPaths: [], - // Defaults each value's source to "ambient" so callers don't need to spell it out. - sources: Object.fromEntries(Object.keys(values).map((key) => [key, sources[key] ?? "ambient"])), - }; -} - -layer(BunServices.layer)("resolveProjectEnvironmentValues", (it) => { - it.effect("returns just the already-loaded values when no extra dotenv files exist", () => - Effect.gen(function* () { - const dirs = yield* project; - const projectEnv = fakeProjectEnv(dirs, { SUPABASE_PROJECT_ID: "from-loader" }); - expect(yield* resolveProjectEnvironmentValues(projectEnv, dirs.root)).toEqual({ - SUPABASE_PROJECT_ID: "from-loader", - }); - }), - ); - - it.effect("fills in a value from a project-root .env file Go's loadNestedEnv would load", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString(path.join(root, ".env"), "SUPABASE_PROJECT_ID=root-env-project\n"); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("root-env-project"); - }), - ); - - it.effect("prefers a supabase/-dir dotenv file over the same key in a project-root file", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root, supabaseDir } = dirs; - yield* fs.writeFileString( - path.join(supabaseDir, ".env"), - "SUPABASE_PROJECT_ID=supabase-dir-project\n", - ); - yield* fs.writeFileString(path.join(root, ".env"), "SUPABASE_PROJECT_ID=root-dir-project\n"); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("supabase-dir-project"); - }), - ); - - it.effect("lets already-resolved projectEnv.values win over anything discovered locally", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString(path.join(root, ".env"), "SUPABASE_PROJECT_ID=root-env-project\n"); - const projectEnv = fakeProjectEnv(dirs, { SUPABASE_PROJECT_ID: "ambient-project" }); - const merged = yield* resolveProjectEnvironmentValues(projectEnv, root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("ambient-project"); - }), - ); - - it.effect("defaults SUPABASE_ENV to development when unset", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString( - path.join(root, ".env.development"), - "SUPABASE_PROJECT_ID=dev-project\n", - ); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("dev-project"); - }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnvRecord({})))), - ); - - it.effect("defaults an empty SUPABASE_ENV to development", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString( - path.join(root, ".env.development"), - "SUPABASE_PROJECT_ID=dev-project\n", - ); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("dev-project"); - }).pipe((body) => withConfigEnv({ SUPABASE_ENV: "" }, body)), - ); - - it.effect("selects the SUPABASE_ENV-named file over the bare .env file", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString(path.join(root, ".env"), "SUPABASE_PROJECT_ID=bare-env-project\n"); - yield* fs.writeFileString( - path.join(root, ".env.production"), - "SUPABASE_PROJECT_ID=prod-project\n", - ); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("prod-project"); - }).pipe((body) => withConfigEnv({ SUPABASE_ENV: "production" }, body)), - ); - - it.effect("prefers the .local variant of the SUPABASE_ENV file over the non-local one", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString( - path.join(root, ".env.production"), - "SUPABASE_PROJECT_ID=prod-project\n", - ); - yield* fs.writeFileString( - path.join(root, ".env.production.local"), - "SUPABASE_PROJECT_ID=prod-local-project\n", - ); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("prod-local-project"); - }).pipe((body) => withConfigEnv({ SUPABASE_ENV: "production" }, body)), - ); - - it.effect("skips .env.local when SUPABASE_ENV=test, matching Go's loadDefaultEnv", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString( - path.join(root, ".env.local"), - "SUPABASE_PROJECT_ID=local-project\n", - ); - yield* fs.writeFileString(path.join(root, ".env.test"), "SUPABASE_PROJECT_ID=test-project\n"); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("test-project"); - }).pipe((body) => withConfigEnv({ SUPABASE_ENV: "test" }, body)), - ); - - it.effect("prefers an explicit supabaseEnv argument over the configured SUPABASE_ENV", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString(path.join(root, ".env"), "SUPABASE_PROJECT_ID=bare-env-project\n"); - yield* fs.writeFileString( - path.join(root, ".env.local"), - "SUPABASE_PROJECT_ID=local-project\n", - ); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root, "test"); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("bare-env-project"); - }).pipe((body) => withConfigEnv({ SUPABASE_ENV: "production" }, body)), - ); - - it.effect("falls back to the configured SUPABASE_ENV when no supabaseEnv argument is given", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString(path.join(root, ".env"), "SUPABASE_PROJECT_ID=bare-env-project\n"); - yield* fs.writeFileString( - path.join(root, ".env.production"), - "SUPABASE_PROJECT_ID=prod-project\n", - ); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("prod-project"); - }).pipe((body) => withConfigEnv({ SUPABASE_ENV: "production" }, body)), - ); - - it.effect("strips quotes the same way the shared dotenv parser does", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString( - path.join(root, ".env"), - 'SUPABASE_AUTH_JWT_SECRET="a quoted value"\n', - ); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_AUTH_JWT_SECRET"]).toBe("a quoted value"); - }), - ); - - it.effect("ignores blank lines and comments", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString( - path.join(root, ".env"), - "\n# a comment\nSUPABASE_PROJECT_ID=commented-project\n", - ); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("commented-project"); - }), - ); - - it.effect( - "preserves a literal # in an unquoted value with no leading whitespace, matching godotenv", - () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString( - path.join(root, ".env"), - "SUPABASE_AUTH_JWT_SECRET=long#secret\n", - ); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_AUTH_JWT_SECRET"]).toBe("long#secret"); - }), - ); - - it.effect("still truncates an unquoted value at a whitespace-preceded inline comment", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString(path.join(root, ".env"), "SUPABASE_PROJECT_ID=54323 # local\n"); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("54323"); - }), - ); - - it.effect("strips a trailing comment after a quoted value, matching godotenv", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString(path.join(root, ".env"), 'SUPABASE_PROJECT_ID="demo" # local\n'); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("demo"); - }), - ); - - it.effect( - "accepts a colon-separated assignment, matching godotenv's YAML-style key/value form", - () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString(path.join(root, ".env"), "SUPABASE_PROJECT_ID: colon-project\n"); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("colon-project"); - }), - ); - - it.effect( - "prefers an env-specific file over a same-key value projectEnv.values sourced from a bare .env file", - () => - Effect.gen(function* () { - // Only an "ambient" source outranks the file precedence computed locally. - const dirs = yield* project; - const { fs, path, root, supabaseDir } = dirs; - yield* fs.writeFileString( - path.join(supabaseDir, ".env.development.local"), - "SUPABASE_PROJECT_ID=env-specific-project\n", - ); - const projectEnv = fakeProjectEnv( - dirs, - { SUPABASE_PROJECT_ID: "bare-dotenv-project" }, - { SUPABASE_PROJECT_ID: ".env" }, - ); - const merged = yield* resolveProjectEnvironmentValues(projectEnv, root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("env-specific-project"); - }).pipe((body) => withConfigEnv({ SUPABASE_ENV: "development" }, body)), - ); - - it.effect("still lets a truly ambient-sourced value win over any file", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root, supabaseDir } = dirs; - yield* fs.writeFileString( - path.join(supabaseDir, ".env.development.local"), - "SUPABASE_PROJECT_ID=env-specific-project\n", - ); - const projectEnv = fakeProjectEnv( - dirs, - { SUPABASE_PROJECT_ID: "ambient-project" }, - { SUPABASE_PROJECT_ID: "ambient" }, - ); - const merged = yield* resolveProjectEnvironmentValues(projectEnv, root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("ambient-project"); - }).pipe((body) => withConfigEnv({ SUPABASE_ENV: "development" }, body)), - ); - - it.effect( - "fails on a malformed line, matching Go's loadEnvIfExists propagating godotenv's parse error", - () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString(path.join(root, ".env"), "not a valid line\n"); - const error = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root).pipe( - Effect.flip, - ); - expect(error).toBeInstanceOf(ProjectEnvironmentError); - expect(String(error)).toMatch(/^Error: failed to parse environment file: /); - }), - ); - - it.effect("keeps a leading BOM, so a BOM-prefixed .env fails to parse", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString(path.join(root, ".env"), "\uFEFFSUPABASE_PROJECT_ID=bom-project\n"); - const error = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root).pipe( - Effect.flip, - ); - expect(String(error)).toMatch(/^Error: failed to parse environment file: /); - }), - ); - - it.effect("fails with the native read error when a dotenv path is a directory", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.makeDirectory(path.join(root, ".env")); - const error = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root).pipe( - Effect.flip, - ); - expect(String(error)).toMatch(/^Error: EISDIR/); - }), - ); - - it.effect("fails with a typed error when SUPABASE_ENV cannot be resolved", () => - Effect.gen(function* () { - const dirs = yield* project; - const error = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), dirs.root).pipe( - Effect.flip, - ); - expect(error).toBeInstanceOf(ProjectEnvironmentError); - expect(error.message).toBe("failed to resolve environment variable: SUPABASE_ENV"); - }).pipe( - Effect.provide( - ConfigProvider.layer( - ConfigProvider.make(() => - Effect.fail(new ConfigProvider.SourceError({ message: "injected" })), - ), - ), - ), - ), - ); - - it.effect("skips a dotenv path that is a symlink loop, like a missing file", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.symlink(path.join(root, ".env"), path.join(root, ".env")); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged).toEqual({}); - }), - ); - - it.effect("expands an unquoted $VAR reference to an earlier value in the same file", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString(path.join(root, ".env"), "BASE=demo\nSUPABASE_PROJECT_ID=$BASE\n"); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("demo"); - }), - ); - - it.effect("expands a braced ${VAR} reference in a double-quoted value", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString( - path.join(root, ".env"), - 'SECRET=shh\nSUPABASE_AUTH_JWT_SECRET="${SECRET}"\n', - ); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_AUTH_JWT_SECRET"]).toBe("shh"); - }), - ); - - it.effect("does not expand variable references inside single-quoted values", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString( - path.join(root, ".env"), - "BASE=demo\nSUPABASE_PROJECT_ID='$BASE'\n", - ); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("$BASE"); - }), - ); - - it.effect( - "expands an unresolved bare reference to an empty string, matching Go's map zero-value", - () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString(path.join(root, ".env"), "SUPABASE_PROJECT_ID=$NOPE\n"); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe(""); - }), - ); - - it.effect( - "expands an unresolved braced reference to an empty string, matching Go's map zero-value", - () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString(path.join(root, ".env"), 'SUPABASE_AUTH_JWT_SECRET="${NOPE}"\n'); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_AUTH_JWT_SECRET"]).toBe(""); - }), - ); - - it.effect( - "preserves a backslash-escaped $VAR reference as a literal, matching godotenv's escape rule", - () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString( - path.join(root, ".env"), - "BASE=demo\nSUPABASE_PROJECT_ID=demo\\$BASE\n", - ); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("demo$BASE"); - }), - ); - - it.effect("preserves a backslash-escaped ${VAR} reference in a double-quoted value", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString( - path.join(root, ".env"), - 'BASE=demo\nSUPABASE_PROJECT_ID="demo\\${BASE}"\n', - ); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("demo${BASE}"); - }), - ); - - it.effect("treats a bare trailing $ with no variable name as a literal", () => - Effect.gen(function* () { - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString(path.join(root, ".env"), "SUPABASE_PROJECT_ID=demo$\n"); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("demo$"); - }), - ); - - it.effect( - "preserves a multiline quoted value alongside an unrelated SUPABASE_* key (godotenv parity)", - () => - Effect.gen(function* () { - // A quoted value spanning physical lines (e.g. a pasted PEM key) must not break - // parsing of the rest of the file. - const pem = "-----BEGIN PRIVATE KEY-----\nMIIBogIBAAJ\n-----END PRIVATE KEY-----"; - const dirs = yield* project; - const { fs, path, root } = dirs; - yield* fs.writeFileString( - path.join(root, ".env"), - `PRIVATE_KEY="${pem}"\nSUPABASE_PROJECT_ID=multiline-safe-project\n`, - ); - const merged = yield* resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("multiline-safe-project"); - }), - ); - - it.effect( - "records the parsed dotenv file count on one resolve span, and no span when tracing is off", - () => - Effect.gen(function* () { - const spans: Array = []; - const tracer = Tracer.make({ - span(options) { - const span = new Tracer.NativeSpan(options); - spans.push(span); - return span; - }, - }); - const dirs = yield* project; - const { fs, path, root, supabaseDir } = dirs; - yield* fs.writeFileString( - path.join(supabaseDir, ".env"), - "SUPABASE_PROJECT_ID=supabase-dir-project\n", - ); - yield* fs.writeFileString( - path.join(root, ".env.development"), - "SUPABASE_AUTH_JWT_SECRET=dev-secret\n", - ); - const resolve = resolveProjectEnvironmentValues(fakeProjectEnv(dirs), root).pipe( - Effect.withTracer(tracer), - ); - const expected = { - SUPABASE_PROJECT_ID: "supabase-dir-project", - SUPABASE_AUTH_JWT_SECRET: "dev-secret", - }; - - expect(yield* resolve.pipe(Effect.withTracerEnabled(false))).toEqual(expected); - expect(spans).toEqual([]); - - expect(yield* resolve.pipe(Effect.withTracerEnabled(true))).toEqual(expected); - expect(spans.map((span) => span.name)).toEqual(["ProjectEnvironment.resolve"]); - expect(spans[0]?.attributes.get("file.count")).toBe(2); - }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnvRecord({})))), - ); - - it.effect("counts a .env.local file once when SUPABASE_ENV is local", () => - Effect.gen(function* () { - const spans: Array = []; - const tracer = Tracer.make({ - span(options) { - const span = new Tracer.NativeSpan(options); - spans.push(span); - return span; - }, - }); - const dirs = yield* project; - const { fs, path, root, supabaseDir } = dirs; - yield* fs.writeFileString( - path.join(supabaseDir, ".env.local"), - "SUPABASE_PROJECT_ID=local-project\n", - ); - const merged = yield* resolveProjectEnvironmentValues( - fakeProjectEnv(dirs), - root, - "local", - ).pipe(Effect.withTracer(tracer), Effect.withTracerEnabled(true)); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("local-project"); - expect(spans[0]?.attributes.get("file.count")).toBe(1); - }), - ); - - describe("when no project was found (projectEnv is null)", () => { - // A missing config.toml must not skip dotenv loading; these cover the fallback - // that derives `/supabase` directly. - - it.effect("still reads a supabase/-dir dotenv file directly under workdir", () => - Effect.gen(function* () { - const { fs, path, root, supabaseDir } = yield* project; - yield* fs.writeFileString( - path.join(supabaseDir, ".env"), - "SUPABASE_PROJECT_ID=fallback-project\n", - ); - const merged = yield* resolveProjectEnvironmentValues(null, root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("fallback-project"); - }).pipe((body) => withEnvVar("SUPABASE_PROJECT_ID", undefined, body)), - ); - - it.effect("still reads a project-root dotenv file directly under workdir", () => - Effect.gen(function* () { - const { fs, path, root } = yield* project; - yield* fs.writeFileString( - path.join(root, ".env"), - "SUPABASE_PROJECT_ID=root-fallback-project\n", - ); - const merged = yield* resolveProjectEnvironmentValues(null, root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("root-fallback-project"); - }).pipe((body) => withEnvVar("SUPABASE_PROJECT_ID", undefined, body)), - ); - - it.effect( - "prefers the supabase/-dir file over the project-root file, same as the non-null case", - () => - Effect.gen(function* () { - const { fs, path, root, supabaseDir } = yield* project; - yield* fs.writeFileString( - path.join(supabaseDir, ".env"), - "SUPABASE_PROJECT_ID=supabase-dir-project\n", - ); - yield* fs.writeFileString( - path.join(root, ".env"), - "SUPABASE_PROJECT_ID=root-dir-project\n", - ); - const merged = yield* resolveProjectEnvironmentValues(null, root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("supabase-dir-project"); - }).pipe((body) => withEnvVar("SUPABASE_PROJECT_ID", undefined, body)), - ); - - it.effect("lets an ambient shell var win over a dotenv value, using process.env directly", () => - Effect.gen(function* () { - const { fs, path, root, supabaseDir } = yield* project; - yield* fs.writeFileString( - path.join(supabaseDir, ".env"), - "SUPABASE_PROJECT_ID=dotenv-fallback-project\n", - ); - const merged = yield* resolveProjectEnvironmentValues(null, root); - expect(merged["SUPABASE_PROJECT_ID"]).toBe("ambient-fallback-project"); - }).pipe((body) => withEnvVar("SUPABASE_PROJECT_ID", "ambient-fallback-project", body)), - ); - - it.effect("returns an empty object when workdir has no dotenv files and no ambient value", () => - Effect.gen(function* () { - const { root } = yield* project; - const merged = yield* resolveProjectEnvironmentValues(null, root); - expect(merged["SUPABASE_PROJECT_ID"]).toBeUndefined(); - }).pipe((body) => withEnvVar("SUPABASE_PROJECT_ID", undefined, body)), - ); - }); -}); diff --git a/apps/cli/src/command-internal/project-target.ts b/apps/cli/src/command-internal/project-target.ts deleted file mode 100644 index 3e25e6d43e..0000000000 --- a/apps/cli/src/command-internal/project-target.ts +++ /dev/null @@ -1,223 +0,0 @@ -import type { SupabaseApiError } from "@supabase/api/effect"; -import { Data, Effect, Option } from "effect"; - -import { RemoteTarget, type RemoteTargetError } from "../config/remote-target.service.ts"; -import { - parentNotLinkedMessage, - parentRefInvalidMessage, - parentRefTypoHint, -} from "./parent-project-ref.ts"; -import { Output } from "../shared/output/output.service.ts"; -import { projectRefPicker } from "../config/remote-target.layer.ts"; -import { branchRef, resolveBranchProjectRef } from "./branch-ref.resolver.ts"; -import { compose, withCondition } from "./command-permissions/model.ts"; -import { sanitizeInlineName } from "./http-errors.ts"; -import { BRANCH_PROJECT_REF_PATTERN, BRANCH_UUID_PATTERN } from "./ref-patterns.ts"; -import { - actionability, - type CliErrorActionabilityDeclaration, - ErrorActionabilityId, -} from "../shared/telemetry/error-actionability.ts"; - -/** - * The resolved comparison/pull target for a command that accepts `--project-ref` - * (`config diff`, `config pull`, `config push`, `pull`): a project ref, plus the branch - * name/UUID it was given, if any. - */ -export interface ConfigTarget { - readonly ref: string; - readonly branch: string | undefined; -} - -/** - * Builds the four target-resolution failures {@link resolveConfigTarget} can raise. - * A single type parameter suffices because {@link configTargetErrorsFor} already unions - * the four minted classes into one type. - */ -export interface ConfigTargetErrors { - /** - * `target` was named as a branch, but no project is linked to search for - * branches under — none of `SUPABASE_PROJECT_ID`, - * `supabase/.temp/linked-project.json`, or `supabase/.temp/project-ref` - * yielded a candidate. - */ - readonly notLinked: (target: string) => TError; - /** - * `target` was named as a branch, and a parent-project candidate exists - * but is not ref-shaped — corrupt or stale linked state. - */ - readonly parentRefInvalid: (target: string) => TError; - /** `target` named a branch the parent project does not have. */ - readonly branchNotFound: (target: string) => TError; - /** The resolved branch has no project ref yet (still provisioning). */ - readonly branchNotReady: (target: string) => TError; -} - -/** The constructor shape every minted target-error class has. */ -type ConfigTargetErrorClass = new (args: { readonly message: string }) => E; - -/** - * Mints one calling command family's four target-resolution error classes from its name - * prefix, keeping `_tag`, fingerprint, and actionability family-owned from one definition. - * - * Every caller must re-export each minted class from its own `*.errors.ts` under the - * family-prefixed name; `error-tag-stability.unit.test.ts`'s static scan only sees the - * template-interpolated tags there. - */ -export function mintConfigTargetErrors(prefix: Prefix) { - class BranchNotFoundError extends Data.TaggedError(`${prefix}BranchNotFoundError`)<{ - readonly message: string; - }> { - get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { - return actionability.invalidInput; - } - } - class BranchNotLinkedError extends Data.TaggedError(`${prefix}BranchNotLinkedError`)<{ - readonly message: string; - }> { - get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { - return actionability.projectNotLinked; - } - } - class ParentRefInvalidError extends Data.TaggedError(`${prefix}ParentRefInvalidError`)<{ - readonly message: string; - }> { - get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { - return actionability.relinkProject; - } - } - class BranchNotReadyError extends Data.TaggedError(`${prefix}BranchNotReadyError`)<{ - readonly message: string; - }> { - get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { - return { ...actionability.apiStatus, fingerprint_suffix: "branch_not_ready" }; - } - } - return { BranchNotFoundError, BranchNotLinkedError, ParentRefInvalidError, BranchNotReadyError }; -} - -/** - * Wraps a family's four minted classes as the `errors` bundle {@link resolveConfigTarget} - * takes. The message text is identical across every family, so it lives here instead of - * being restated per handler. - */ -export function configTargetErrorsFor< - TNotLinked, - TParentRefInvalid, - TBranchNotFound, - TBranchNotReady, ->(classes: { - readonly notLinked: ConfigTargetErrorClass; - readonly parentRefInvalid: ConfigTargetErrorClass; - readonly branchNotFound: ConfigTargetErrorClass; - readonly branchNotReady: ConfigTargetErrorClass; -}): ConfigTargetErrors { - return { - notLinked: (target) => new classes.notLinked({ message: parentNotLinkedMessage(target) }), - parentRefInvalid: (target) => - new classes.parentRefInvalid({ message: parentRefInvalidMessage(target) }), - branchNotFound: (target) => - new classes.branchNotFound({ - message: `Branch "${sanitizeInlineName(target)}" not found. Run \`supabase branches list\` to see available branches.${parentRefTypoHint(target)}`, - }), - branchNotReady: (target) => - new classes.branchNotReady({ - message: `Branch "${sanitizeInlineName(target)}" has no project ref yet. Wait for it to finish provisioning, then retry.`, - }), - }; -} - -/** - * What {@link resolveConfigTarget} needs to know about a branch-lookup failure to decide - * whether it was a 404. `status` is optional because only one arm of the `mapHttpError` - * failure union carries an HTTP status; every other producer has none. `_tag` is required - * so the interface isn't a TypeScript weak type. - */ -export interface ConfigTargetResolveFailure { - readonly _tag: string; - readonly status?: number | undefined; -} - -// Not folded into `mapResolveError`: `config pull` shares one mapper across the branch -// lookup and the config read, so treating every 404 as "branch not found" here would -// misclassify a config-read 404. -function reclassifyBranchNotFoundError( - cause: E, - notFoundError: C, -): Effect.Effect { - return cause.status === 404 ? Effect.fail(notFoundError) : Effect.fail(cause); -} - -/** The calls `resolveConfigTarget` makes: a branch lookup for a non-ref `--project-ref`, the project picker when no ref is given or linked. */ -export const configTarget = compose( - withCondition(branchRef, { flag: "project-ref", present: true }), - projectRefPicker, -); - -/** - * Resolves `--project-ref` to a {@link ConfigTarget}: a project ref, or the name/UUID of a - * branch on the linked project. A ref-shaped value (20 lowercase letters) is always a project ref. - * - * A UUID branch needs no parent ref, so it works unlinked; a name branch resolves the parent - * ref eagerly, before any spinner starts, so an unlinked or stale link fails immediately. - */ -export const resolveConfigTarget = Effect.fn("ProjectTarget.resolve")(function* < - TError, - EResolve extends ConfigTargetResolveFailure, ->( - requested: Option.Option, - errors: ConfigTargetErrors, - /** Maps a branch-lookup (`GET`-by-UUID or `FIND`-by-name) transport/status failure. */ - mapResolveError: (cause: SupabaseApiError) => Effect.Effect, -) { - const output = yield* Output; - const remoteTarget = yield* RemoteTarget; - - let ref: string; - let branch: string | undefined; - if (Option.isSome(requested) && !BRANCH_PROJECT_REF_PATTERN.test(requested.value)) { - const target = requested.value; - branch = target; - const byId = BRANCH_UUID_PATTERN.test(target); - yield* Effect.annotateCurrentSpan("project_target.kind", byId ? "branch_id" : "branch_name"); - - let parentRef: Effect.Effect; - if (byId) { - parentRef = remoteTarget - .resolveParentScoped(Option.none()) - .pipe(Effect.map((resolved) => resolved.ref)); - } else { - const parent = yield* remoteTarget.parent; - if (parent.kind === "absent") { - return yield* Effect.fail(errors.notLinked(target)); - } - if (parent.kind === "invalid") { - return yield* Effect.fail(errors.parentRefInvalid(target)); - } - parentRef = Effect.succeed(parent.ref); - } - - const resolving = - output.format === "text" ? yield* output.task("Resolving branch...") : undefined; - ref = yield* resolveBranchProjectRef(target, parentRef, { - mapGetError: mapResolveError, - mapFindError: mapResolveError, - }).pipe( - Effect.tapError(() => resolving?.fail() ?? Effect.void), - Effect.catch((cause) => reclassifyBranchNotFoundError(cause, errors.branchNotFound(target))), - ); - yield* resolving?.clear ?? Effect.void; - - // The resolved branch might not have a project ref yet (still provisioning); don't - // let an empty ref reach the config-read call. - if (!BRANCH_PROJECT_REF_PATTERN.test(ref)) { - return yield* Effect.fail(errors.branchNotReady(target)); - } - } else { - yield* Effect.annotateCurrentSpan("project_target.kind", "project"); - ({ ref } = yield* remoteTarget.resolve(requested)); - } - - yield* Effect.annotateCurrentSpan("project.ref", ref); - return { ref, branch }; -}); diff --git a/apps/cli/src/command-internal/project-target.unit.test.ts b/apps/cli/src/command-internal/project-target.unit.test.ts deleted file mode 100644 index 74db5949b4..0000000000 --- a/apps/cli/src/command-internal/project-target.unit.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Unit tests for the `mintConfigTargetErrors` factory and `configTargetErrorsFor` - * builder, independent of any family's own `*.errors.ts` file. - */ - -import { describe, expect, it } from "vitest"; - -import { actionability, ErrorActionabilityId } from "../shared/telemetry/error-actionability.ts"; -import { configTargetErrorsFor, mintConfigTargetErrors } from "./project-target.ts"; - -const PREFIX = "ConfigTestMint"; - -describe("mintConfigTargetErrors", () => { - const classes = mintConfigTargetErrors(PREFIX); - - it("tags BranchNotFoundError as `${prefix}BranchNotFoundError`", () => { - expect(new classes.BranchNotFoundError({ message: "x" })._tag).toBe( - "ConfigTestMintBranchNotFoundError", - ); - }); - - it("tags BranchNotLinkedError as `${prefix}BranchNotLinkedError`", () => { - expect(new classes.BranchNotLinkedError({ message: "x" })._tag).toBe( - "ConfigTestMintBranchNotLinkedError", - ); - }); - - it("tags ParentRefInvalidError as `${prefix}ParentRefInvalidError`", () => { - expect(new classes.ParentRefInvalidError({ message: "x" })._tag).toBe( - "ConfigTestMintParentRefInvalidError", - ); - }); - - it("tags BranchNotReadyError as `${prefix}BranchNotReadyError`", () => { - expect(new classes.BranchNotReadyError({ message: "x" })._tag).toBe( - "ConfigTestMintBranchNotReadyError", - ); - }); - - it("declares BranchNotFoundError as actionability.invalidInput", () => { - const instance = new classes.BranchNotFoundError({ message: "x" }); - expect(instance[ErrorActionabilityId]).toEqual(actionability.invalidInput); - }); - - it("declares BranchNotLinkedError as actionability.projectNotLinked", () => { - const instance = new classes.BranchNotLinkedError({ message: "x" }); - expect(instance[ErrorActionabilityId]).toEqual(actionability.projectNotLinked); - }); - - it("declares ParentRefInvalidError as actionability.relinkProject", () => { - const instance = new classes.ParentRefInvalidError({ message: "x" }); - expect(instance[ErrorActionabilityId]).toEqual(actionability.relinkProject); - }); - - it("declares BranchNotReadyError as actionability.apiStatus with a branch_not_ready fingerprint suffix", () => { - const instance = new classes.BranchNotReadyError({ message: "x" }); - expect(instance[ErrorActionabilityId]).toEqual({ - ...actionability.apiStatus, - fingerprint_suffix: "branch_not_ready", - }); - }); - - it("mints genuinely per-call classes, not a cached/shared set, across different prefixes", () => { - const a = mintConfigTargetErrors("ConfigTestMintA"); - const b = mintConfigTargetErrors("ConfigTestMintB"); - const aTag = new a.BranchNotFoundError({ message: "x" })._tag; - const bTag = new b.BranchNotFoundError({ message: "x" })._tag; - expect(aTag).toBe("ConfigTestMintABranchNotFoundError"); - expect(bTag).toBe("ConfigTestMintBBranchNotFoundError"); - expect(aTag).not.toBe(bTag); - }); -}); - -describe("configTargetErrorsFor", () => { - it("wraps a minted class set into constructors that produce working, correctly tagged instances", () => { - const classes = mintConfigTargetErrors(PREFIX); - const errors = configTargetErrorsFor({ - notLinked: classes.BranchNotLinkedError, - parentRefInvalid: classes.ParentRefInvalidError, - branchNotFound: classes.BranchNotFoundError, - branchNotReady: classes.BranchNotReadyError, - }); - - const error = errors.branchNotFound("some-target"); - expect(error._tag).toBe("ConfigTestMintBranchNotFoundError"); - expect(error.message).toContain("some-target"); - }); -}); diff --git a/apps/cli/src/command-internal/prompt-yes-no.ts b/apps/cli/src/command-internal/prompt-yes-no.ts index d50fc1e741..749d10b056 100644 --- a/apps/cli/src/command-internal/prompt-yes-no.ts +++ b/apps/cli/src/command-internal/prompt-yes-no.ts @@ -21,6 +21,15 @@ export const parseYesNo = (input: string): boolean | undefined => { return undefined; }; +interface YesNoOutcome { + readonly value: boolean; + /** `false` when the default was taken because nothing answered (EOF, timeout, no prompt). */ + readonly answered: boolean; +} + +const answered = (value: boolean): YesNoOutcome => ({ value, answered: true }); +const unanswered = (value: boolean): YesNoOutcome => ({ value, answered: false }); + /** * Confirm-or-default prompt shared by command handlers and shell-agnostic code alike. * `yes` echoes an affirmative answer and returns `true` immediately; non-text output @@ -30,7 +39,7 @@ export const parseYesNo = (input: string): boolean | undefined => { * wins and an empty line takes the default. Any other line declines, except under * `interactive: false`, where it takes the default. */ -export const promptYesNo = Effect.fnUntraced(function* ( +export const promptYesNoOutcome = Effect.fnUntraced(function* ( output: typeof Output.Service, yes: boolean, label: string, @@ -41,14 +50,14 @@ export const promptYesNo = Effect.fnUntraced(function* ( const choices = defaultValue ? "Y/n" : "y/N"; if (yes) { yield* output.raw(`${label} [${choices}] y\n`, "stderr"); - return true; + return answered(true); } if (output.format !== "text" && !options.readMachineStdin) { - return defaultValue; + return unanswered(defaultValue); } const tty = yield* Tty; if (output.format !== "text" && (!interactive || tty.stdinIsTty)) { - return defaultValue; + return unanswered(defaultValue); } // Text `interactive: false` still prints the label and reads one line instead of // silently returning the default — it uses the same non-TTY read path below. @@ -60,9 +69,15 @@ export const promptYesNo = Effect.fnUntraced(function* ( yield* output.raw(`${input.trim()}\n`, "stderr"); // An unrecognised answer is never consent; under `interactive: false` the line may be // the caller's own script text, so it keeps the default. - return parseYesNo(input) ?? (interactive && input.length > 0 ? false : defaultValue); + const value = parseYesNo(input) ?? (interactive && input.length > 0 ? false : defaultValue); + return Option.isSome(line) ? answered(value) : unanswered(value); } - return yield* output - .promptConfirm(label, { defaultValue }) - .pipe(Effect.catchTag("NonInteractiveError", () => Effect.succeed(defaultValue))); + return yield* output.promptConfirm(label, { defaultValue }).pipe( + Effect.map(answered), + Effect.catchTag("NonInteractiveError", () => Effect.succeed(unanswered(defaultValue))), + ); }); + +/** {@link promptYesNoOutcome} reduced to the boolean every other caller wants. */ +export const promptYesNo = (...args: Parameters) => + promptYesNoOutcome(...args).pipe(Effect.map((outcome) => outcome.value)); diff --git a/apps/cli/src/command-internal/prompt-yes-no.unit.test.ts b/apps/cli/src/command-internal/prompt-yes-no.unit.test.ts index 2af549adfc..74c74c61da 100644 --- a/apps/cli/src/command-internal/prompt-yes-no.unit.test.ts +++ b/apps/cli/src/command-internal/prompt-yes-no.unit.test.ts @@ -5,7 +5,7 @@ import { mockOutput, mockStdin, mockTty } from "../../tests/helpers/mocks.ts"; import { Output } from "../shared/output/output.service.ts"; import { Stdin } from "../shared/runtime/stdin.service.ts"; -import { parseYesNo, promptYesNo } from "./prompt-yes-no.ts"; +import { parseYesNo, promptYesNo, promptYesNoOutcome } from "./prompt-yes-no.ts"; describe("parseYesNo", () => { it("parses affirmative answers (case-insensitive, trimmed)", () => { @@ -108,6 +108,34 @@ describe("promptYesNo machine consent", () => { } }); +describe("promptYesNoOutcome piped answers", () => { + const outcome = (piped: string) => + Effect.gen(function* () { + const output = yield* Output; + return yield* promptYesNoOutcome(output, false, "Confirm?", false); + }).pipe( + Effect.provide( + Layer.mergeAll(mockOutput().layer, mockStdin(false, piped), mockTty({ stdinIsTty: false })), + ), + ); + + it.effect("reports closed stdin as unanswered, taking the default", () => + Effect.gen(function* () { + expect(yield* outcome("")).toEqual({ value: false, answered: false }); + }), + ); + + it.effect.each([ + ["\n", false], + ["n\n", false], + ["y\n", true], + ] as const)("reports the piped line %j as an answer", ([piped, value]) => + Effect.gen(function* () { + expect(yield* outcome(piped)).toEqual({ value, answered: true }); + }), + ); +}); + describe("promptYesNo piped text answers", () => { const ask = (piped: string, defaultValue: boolean, interactive = true) => Effect.gen(function* () { diff --git a/apps/cli/src/command-internal/removed-command.ts b/apps/cli/src/command-internal/removed-command.ts new file mode 100644 index 0000000000..563a2df1a8 --- /dev/null +++ b/apps/cli/src/command-internal/removed-command.ts @@ -0,0 +1,66 @@ +import { Data, Effect, Option } from "effect"; +import { + actionability, + type CliErrorActionabilityDeclaration, + ErrorActionabilityId, +} from "../shared/telemetry/error-actionability.ts"; +import { + CommandRuntime, + getCommandRuntimeCommand, +} from "../shared/runtime/command-runtime.service.ts"; +import { TelemetryState } from "../telemetry/telemetry-state.service.ts"; + +/** + * A tombstoned command path or a rejected removed flag. The caller supplies its own + * replacement suggestion; there is no closed vocabulary for it since removal + * suggestions vary per surface. + */ +export class RemovedSurfaceError extends Data.TaggedError("RemovedSurfaceError")<{ + readonly message: string; + readonly suggestion: string; + readonly kind: "command" | "flag"; +}> { + get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { + return { + ...actionability.removedSurface, + fingerprint_suffix: this.kind === "flag" ? "removed_flag" : "removed_command", + }; + } +} + +/** + * Fails with a `RemovedSurfaceError` naming the invoked command path. The caller wraps the + * returned effect in `withCommandTelemetry()` then `withJsonErrorHandling`, matching + * `commit.command.ts`'s composition order, and provides a `CommandRuntime` layer for its path. + * Flushes `TelemetryState` itself (`Effect.ensuring`), since a tombstoned command never reaches + * its own handler's finalizer wiring. + */ +export const removedCommand = (suggestion: string) => + Effect.gen(function* () { + const telemetryState = yield* TelemetryState; + return yield* Effect.gen(function* () { + const commandRuntime = yield* CommandRuntime; + const command = getCommandRuntimeCommand(commandRuntime); + return yield* new RemovedSurfaceError({ + message: `supabase ${command} was removed.`, + suggestion, + kind: "command", + }); + }).pipe(Effect.ensuring(telemetryState.flush)); + }); + +/** + * Fails with a `RemovedSurfaceError` for a removed flag on an otherwise-native command. The host + * command already wraps its whole handler in `withCommandTelemetry`, so this is a bare effect. + * Flushes `TelemetryState` itself (`Effect.ensuring`) when the command provides one: call sites + * check this before their own handler reaches its own finalizer wiring further down. + */ +export const removedFlag = (flag: string, suggestion: string) => + Effect.gen(function* () { + const telemetryState = yield* Effect.serviceOption(TelemetryState); + return yield* Effect.fail( + new RemovedSurfaceError({ message: `${flag} was removed.`, suggestion, kind: "flag" }), + ).pipe( + Effect.ensuring(Option.isSome(telemetryState) ? telemetryState.value.flush : Effect.void), + ); + }); diff --git a/apps/cli/src/command-internal/removed-command.unit.test.ts b/apps/cli/src/command-internal/removed-command.unit.test.ts new file mode 100644 index 0000000000..b5d2935726 --- /dev/null +++ b/apps/cli/src/command-internal/removed-command.unit.test.ts @@ -0,0 +1,103 @@ +import { BunCrypto } from "@effect/platform-bun"; +import { describe, expect, it } from "@effect/vitest"; +import { Effect, Exit, Layer } from "effect"; +import { classifyCliErrorActionability } from "../shared/telemetry/error-actionability.ts"; +import { commandRuntimeLayer } from "../shared/runtime/command-runtime.layer.ts"; +import { mockTelemetryStateTracked } from "../../tests/helpers/command-mocks.ts"; +import { removedCommand, removedFlag, RemovedSurfaceError } from "./removed-command.ts"; + +describe("RemovedSurfaceError actionability", () => { + it("classifies a removed command with the :removed_command fingerprint suffix", () => { + const classified = classifyCliErrorActionability( + new RemovedSurfaceError({ + message: "supabase db branch create was removed.", + suggestion: "Use `supabase branches --help` instead.", + kind: "command", + }), + ); + expect(classified).toMatchObject({ + error_kind: "user_actionable", + error_category: "invalid_input", + has_suggestion: true, + suggestion_type: "run_command", + }); + expect(classified.error_fingerprint).toBe("tag:RemovedSurfaceError:removed_command"); + }); + + it("classifies a removed flag with the :removed_flag fingerprint suffix", () => { + const classified = classifyCliErrorActionability( + new RemovedSurfaceError({ + message: "--use-pg-schema was removed.", + suggestion: "Use the default pg-delta engine or --use-migra.", + kind: "flag", + }), + ); + expect(classified).toMatchObject({ + error_kind: "user_actionable", + error_category: "invalid_input", + has_suggestion: true, + suggestion_type: "run_command", + }); + expect(classified.error_fingerprint).toBe("tag:RemovedSurfaceError:removed_flag"); + }); + + it("classifies a field-less probe instance without throwing", () => { + const probe = Object.create(RemovedSurfaceError.prototype) as RemovedSurfaceError; + expect(() => classifyCliErrorActionability(probe)).not.toThrow(); + const classified = classifyCliErrorActionability(probe); + expect(classified.error_fingerprint.endsWith(":removed_command")).toBe(true); + expect(classified).toMatchObject({ + error_kind: "user_actionable", + error_category: "invalid_input", + }); + }); +}); + +describe("TelemetryState flush", () => { + it.effect("flushes on a removedCommand failure", () => { + const telemetry = mockTelemetryStateTracked(); + return removedCommand("Use `supabase branches --help` instead.").pipe( + Effect.exit, + Effect.tap((exit) => + Effect.sync(() => { + expect(Exit.isFailure(exit)).toBe(true); + expect(telemetry.flushed).toBe(true); + expect(telemetry.flushCount).toBe(1); + }), + ), + Effect.provide( + Layer.mergeAll( + commandRuntimeLayer(["db", "branch", "create"]).pipe(Layer.provide(BunCrypto.layer)), + telemetry.layer, + ), + ), + ); + }); + + it.effect("flushes on a removedFlag failure", () => { + const telemetry = mockTelemetryStateTracked(); + return removedFlag("--use-pg-schema", "Use the default pg-delta engine or --use-migra.").pipe( + Effect.exit, + Effect.tap((exit) => + Effect.sync(() => { + expect(Exit.isFailure(exit)).toBe(true); + expect(telemetry.flushed).toBe(true); + expect(telemetry.flushCount).toBe(1); + }), + ), + Effect.provide(telemetry.layer), + ); + }); + + it.effect("fails a removedFlag for a command that provides no TelemetryState", () => + removedFlag("--create-ticket", "Report CLI problems with `supabase issue bug`.").pipe( + Effect.flip, + Effect.tap((error) => + Effect.sync(() => { + expect(error).toBeInstanceOf(RemovedSurfaceError); + expect(error.message).toBe("--create-ticket was removed."); + }), + ), + ), + ); +}); diff --git a/apps/cli/src/command-internal/resolve-token.unit.test.ts b/apps/cli/src/command-internal/resolve-token.unit.test.ts index 8e2cb29fd1..e52ce3b99c 100644 --- a/apps/cli/src/command-internal/resolve-token.unit.test.ts +++ b/apps/cli/src/command-internal/resolve-token.unit.test.ts @@ -20,7 +20,6 @@ const settings = (accessToken: Option.Option>) => workdir: "/tmp/supabase-cli-resolve-token", explicitWorkdir: false, workdirEnvValue: Option.none(), - dbPassword: Option.none(), githubToken: Option.none(), userAgent: "SupabaseCLI/test", }); diff --git a/apps/cli/src/command-internal/resolved-config-context.ts b/apps/cli/src/command-internal/resolved-config-context.ts new file mode 100644 index 0000000000..dfd48e1982 --- /dev/null +++ b/apps/cli/src/command-internal/resolved-config-context.ts @@ -0,0 +1,86 @@ +import type { CliConfig } from "@supabase/config"; +import { Effect, Option, Predicate } from "effect"; + +import { CliConfigKeys } from "../config/cli-config-keys.ts"; +import { CliConfigValues } from "../config/cli-config-values.service.ts"; +import type { ResolvedCliConfig } from "../config/cli-config-values.service.ts"; +import { CliConfigLoadError } from "../shared/config/cli-config-load.errors.ts"; +import { configParseCauseMessage } from "../shared/output/normalize-error.ts"; +import { getHostname } from "./hostname.ts"; + +/** The resolved config with the materialized config, project env values and declared document its readers take. */ +interface ResolvedConfigContext { + readonly resolvedConfig: ResolvedCliConfig; + readonly config: CliConfig; + readonly projectEnvValues: Readonly>; + readonly document: Record | undefined; +} + +/** Loads the resolved config for a command that targets no project, so no `[remotes.*]` block applies. */ +export const loadResolvedConfigContext = Effect.fn("ResolvedConfigContext.load")(function* ( + workdir: string, + projectRef: Option.Option = Option.none(), +) { + const values = yield* CliConfigValues; + const resolvedConfig = yield* values.load({ workdir, projectRef }); + return { + resolvedConfig, + config: resolvedConfig.materialized.config, + projectEnvValues: resolvedConfig.projectEnvValues, + document: resolvedConfig.loaded.document, + } satisfies ResolvedConfigContext; +}); + +/** A {@link ResolvedConfigContext} plus the machine hostname and sanitized project id local Docker naming needs. */ +export interface LocalResolvedConfigContext extends ResolvedConfigContext { + readonly hostname: string; + readonly projectId: string; +} + +export const loadLocalResolvedConfigContext = Effect.fn("LocalResolvedConfigContext.load")( + function* (workdir: string, projectRef: Option.Option = Option.none()) { + const context = yield* loadResolvedConfigContext(workdir, projectRef); + const hostname = yield* getHostname(context.resolvedConfig).pipe( + Effect.mapError( + (cause) => + new CliConfigLoadError({ message: `failed to resolve hostname: ${cause.message}` }), + ), + ); + const projectId = (yield* context.resolvedConfig.get(CliConfigKeys.projectId)).value; + return { ...context, hostname, projectId } satisfies LocalResolvedConfigContext; + }, +); + +/** `auth.passkey` and `auth.webauthn` exist only when their tables do; an env override never creates one. */ +export const resolvePasskeyWebauthn = Effect.fn("ResolvedConfigContext.passkeyWebauthn")(function* ( + resolvedConfig: ResolvedCliConfig, +) { + const passkeyEnabled = resolvedConfig.declares("auth.passkey") + ? (yield* resolvedConfig.get(CliConfigKeys.auth.passkey.enabled)).value + : undefined; + const webauthn = resolvedConfig.declares("auth.webauthn") + ? { + rpId: (yield* resolvedConfig.get(CliConfigKeys.auth.webauthn.rpId)).value, + rpDisplayName: (yield* resolvedConfig.get(CliConfigKeys.auth.webauthn.rpDisplayName)).value, + rpOrigins: (yield* resolvedConfig.get(CliConfigKeys.auth.webauthn.rpOrigins)).value, + } + : undefined; + return { passkeyEnabled, webauthn }; +}); + +/** The failure text commands report when the resolved config cannot be loaded. */ +export const describeConfigLoadFailure = (cause: unknown): string => + Predicate.isTagged(cause, "CliConfigParseError") && "cause" in cause + ? `failed to read config: ${configParseCauseMessage(cause.cause)}` + : typeof cause === "object" && + cause !== null && + "_tag" in cause && + (cause._tag === "CliConfigValueError" || + cause._tag === "CliConfigFlagConflictError" || + cause._tag === "CliConfigLoadError" || + cause._tag === "CliConfigRemovedKeyError" || + cause._tag === "CliConfigRemovedEnvError") && + "message" in cause && + typeof cause.message === "string" + ? cause.message + : `failed to read config: ${String(cause)}`; diff --git a/apps/cli/src/command-internal/resolved-config-context.unit.test.ts b/apps/cli/src/command-internal/resolved-config-context.unit.test.ts new file mode 100644 index 0000000000..893c5dc73d --- /dev/null +++ b/apps/cli/src/command-internal/resolved-config-context.unit.test.ts @@ -0,0 +1,98 @@ +import { BunServices } from "@effect/platform-bun"; +import { describe, expect, it } from "@effect/vitest"; +import { Effect, FileSystem, Layer, Path } from "effect"; + +import { useTempWorkdir } from "../../tests/helpers/command-mocks.ts"; +import { mockOutput, processEnvLayer } from "../../tests/helpers/mocks.ts"; +import { CliConfigFlagInputs, makeCliConfigFlagInputs } from "../config/cli-config-flags.ts"; +import { cliConfigValuesLayer } from "../config/cli-config-values.layer.ts"; +import { runtimeInfoLayer } from "../shared/runtime/runtime-info.layer.ts"; +import { loadLocalResolvedConfigContext } from "./resolved-config-context.ts"; + +const tempRoot = useTempWorkdir("supabase-resolved-config-context-"); + +const writeProjectEnv = (workdir: string, contents: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const supabaseDir = path.join(workdir, "supabase"); + yield* fs.makeDirectory(supabaseDir, { recursive: true }); + yield* fs.writeFileString(path.join(supabaseDir, ".env"), contents); + }).pipe(Effect.provide(BunServices.layer)); + +/** Runs against a shell environment of exactly `env`, with the real config service and an empty Docker config dir. */ +const layerWithShellEnv = (workdir: string, env: Readonly> = {}) => + Layer.fresh( + Layer.mergeAll( + BunServices.layer, + runtimeInfoLayer, + cliConfigValuesLayer.pipe( + Layer.provide( + Layer.mergeAll( + BunServices.layer, + mockOutput().layer, + Layer.succeed(CliConfigFlagInputs, makeCliConfigFlagInputs()), + ), + ), + ), + processEnvLayer({ DOCKER_CONFIG: `${workdir}/docker-config`, ...env }), + ), + ); + +describe("loadLocalResolvedConfigContext hostname", () => { + it.effect("reads SUPABASE_SERVICES_HOSTNAME from a project .env", () => + Effect.gen(function* () { + const workdir = tempRoot.current; + yield* writeProjectEnv(workdir, "SUPABASE_SERVICES_HOSTNAME=dotenv-host\n"); + + return yield* loadLocalResolvedConfigContext(workdir).pipe( + Effect.map((context) => { + expect(context.hostname).toBe("dotenv-host"); + }), + Effect.provide(layerWithShellEnv(workdir)), + ); + }), + ); + + it.effect("prefers a shell SUPABASE_SERVICES_HOSTNAME over a project .env", () => + Effect.gen(function* () { + const workdir = tempRoot.current; + yield* writeProjectEnv(workdir, "SUPABASE_SERVICES_HOSTNAME=dotenv-host\n"); + + return yield* loadLocalResolvedConfigContext(workdir).pipe( + Effect.map((context) => { + expect(context.hostname).toBe("shell-host"); + }), + Effect.provide(layerWithShellEnv(workdir, { SUPABASE_SERVICES_HOSTNAME: "shell-host" })), + ); + }), + ); + + it.effect("ignores a project .env DOCKER_HOST when discovering the daemon", () => + Effect.gen(function* () { + const workdir = tempRoot.current; + yield* writeProjectEnv(workdir, "DOCKER_HOST=tcp://x:2375\n"); + + return yield* loadLocalResolvedConfigContext(workdir).pipe( + Effect.map((context) => { + expect(context.hostname).toBe("127.0.0.1"); + }), + Effect.provide(layerWithShellEnv(workdir)), + ); + }), + ); + + it.effect("takes the daemon host from a shell DOCKER_HOST", () => + Effect.gen(function* () { + const workdir = tempRoot.current; + yield* writeProjectEnv(workdir, "DOCKER_HOST=tcp://project-dotenv-host:2375\n"); + + return yield* loadLocalResolvedConfigContext(workdir).pipe( + Effect.map((context) => { + expect(context.hostname).toBe("shell-daemon-host"); + }), + Effect.provide(layerWithShellEnv(workdir, { DOCKER_HOST: "tcp://shell-daemon-host:2375" })), + ); + }), + ); +}); diff --git a/apps/cli/src/command-internal/schema-flags.ts b/apps/cli/src/command-internal/schema-flags.ts index 9aa44377ce..36b014a822 100644 --- a/apps/cli/src/command-internal/schema-flags.ts +++ b/apps/cli/src/command-internal/schema-flags.ts @@ -1,8 +1,7 @@ /** - * Normalizes a repeated `--schema` flag into a flat list, matching the CSV-per-occurrence - * parsing every `--schema`-accepting delegated Go subprocess expects (shared logic lives in + * Normalizes a repeated `--schema` flag into a flat list (shared logic lives in * `string-slice-flag.ts`). Also provides `schemaToCsvField`, the CSV re-encoder used when - * forwarding `--schema` back to that subprocess. + * printing a `--schema` value back into a suggested shell command. * * Shared by `gen types`, `db lint`, `db dump`, `db pull`, `db diff`, and `db schema * {generate,sync}`. @@ -13,9 +12,10 @@ export { StringSliceFlagParseError as SchemaFlagParseError }; export const parseSchemaFlags = parseStringSliceFlag; -// Whether a CSV field must be quoted (matches the `encoding/csv` writer a delegated Go -// subprocess re-parses): never quote the empty string; always quote `\.`; quote when the -// field contains `,`, `"`, `\r`, or `\n`; otherwise quote when the first rune is whitespace. +// Whether a CSV field must be quoted (so a printed +// suggestion round-trips through the same CSV parsing rules `--schema` values use): never quote +// the empty string; always quote `\.`; quote when the field contains `,`, `"`, `\r`, or `\n`; +// otherwise quote when the first character is whitespace. function fieldNeedsQuotes(field: string): boolean { if (field === "") return false; if (field === "\\.") return true; @@ -26,9 +26,8 @@ function fieldNeedsQuotes(field: string): boolean { /** * Serializes a single parsed schema value back into one CSV field — the inverse of * `readAsCSVStrict` for one element. A schema parsed from `--schema '"tenant,one"'` is the - * single value `tenant,one`; forwarding it raw to a delegated Go subprocess would let pflag's - * `StringSlice` re-parse it as CSV and split it into two schemas, so this re-encodes it to - * keep it one field when rebuilding argv for `db diff`/`db pull`. + * single value `tenant,one`; printing it raw into a suggested `--schema` command would let the + * next CSV parse re-split it into two schemas, so this re-encodes it to keep it one field. */ export function schemaToCsvField(value: string): string { if (!fieldNeedsQuotes(value)) return value; diff --git a/apps/cli/src/command-internal/schema-flags.unit.test.ts b/apps/cli/src/command-internal/schema-flags.unit.test.ts index 54d19e95f4..12dd8e8309 100644 --- a/apps/cli/src/command-internal/schema-flags.unit.test.ts +++ b/apps/cli/src/command-internal/schema-flags.unit.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { parseSchemaFlags, SchemaFlagParseError, schemaToCsvField } from "./schema-flags.ts"; -describe("parseSchemaFlags (pflag StringSlice CSV parity)", () => { +describe("parseSchemaFlags", () => { it("splits unquoted comma-separated values", () => { expect(parseSchemaFlags(["public,private"])).toEqual(["public", "private"]); }); @@ -34,7 +34,7 @@ describe("parseSchemaFlags (pflag StringSlice CSV parity)", () => { expect(parseSchemaFlags([])).toEqual([]); }); - it("preserves whitespace (Go does not trim)", () => { + it("preserves whitespace (no trimming)", () => { expect(parseSchemaFlags([" public , private "])).toEqual([" public ", " private "]); }); @@ -63,7 +63,7 @@ describe("schemaToCsvField (inverse — re-encode one value as a CSV field)", () expect(schemaToCsvField("public")).toBe("public"); }); - it("leaves the empty string unquoted (Go csv.Writer)", () => { + it("leaves the empty string unquoted", () => { expect(schemaToCsvField("")).toBe(""); }); diff --git a/apps/cli/src/command-internal/scratch-paths.ts b/apps/cli/src/command-internal/scratch-paths.ts index 2ea3189c26..db9d3a1697 100644 --- a/apps/cli/src/command-internal/scratch-paths.ts +++ b/apps/cli/src/command-internal/scratch-paths.ts @@ -1,7 +1,7 @@ import { Crypto, Effect, Path } from "effect"; import { CommandSettings } from "../config/command-settings.service.ts"; -import { legacyProjectStatePaths } from "./legacy-project-state.paths.ts"; +import { legacyProjectStatePaths } from "../shared/config/legacy-project-state.paths.ts"; import { usesStackState } from "./stack-backend.ts"; /** Host scratch for bundler output and downloads: `$SUPABASE_HOME/tmp` in stack state, else `supabase/.temp`. */ diff --git a/apps/cli/src/command-internal/seed-buckets.ts b/apps/cli/src/command-internal/seed-buckets.ts index 3d0b307e4d..8e0ced02b7 100644 --- a/apps/cli/src/command-internal/seed-buckets.ts +++ b/apps/cli/src/command-internal/seed-buckets.ts @@ -1,16 +1,15 @@ import { type CliConfig, CliConfigSchema } from "@supabase/config/effect"; -import { loadCliConfig, type InternalLoadCliConfigOptions } from "@supabase/config/internal"; import { BunPath } from "@effect/platform-bun"; -import { Effect, FileSystem, Path, Schema } from "effect"; +import { Effect, FileSystem, Option, Path, Schema } from "effect"; import type { PlatformError } from "effect/PlatformError"; import { Output } from "../shared/output/output.service.ts"; import { resolveYesWithProjectEnv } from "./global-flags.ts"; import { CommandSettings } from "../config/command-settings.service.ts"; import { bold, yellow } from "./colors.ts"; -import { loadProjectEnv } from "./db-config.toml-read.ts"; -import { relativeConfigPath } from "./workdir-project.ts"; -import { shouldSearchAncestors } from "./workdir-search.ts"; +import { describeConfigLoadFailure, loadResolvedConfigContext } from "./resolved-config-context.ts"; +import { isConfigValueFailure } from "./config-value-passthrough.ts"; +import { loadCliProjectEnvFiles } from "../shared/config/cli-config-env.ts"; import { promptYesNo } from "./prompt-yes-no.ts"; import { resolveStorageCredentials, @@ -33,8 +32,12 @@ import { isVectorBucketsFeatureNotEnabled, } from "../commands/seed/buckets/buckets.classify.ts"; import { SeedConfigLoadError } from "../commands/seed/buckets/buckets.errors.ts"; +import { describeConfigParseError } from "./workdir-project.ts"; import { bucketObjectKey } from "../commands/seed/buckets/buckets.upload.ts"; -import { configParseCauseMessage } from "../shared/output/normalize-error.ts"; + +// Re-exported so `stack start` can report a project-context config-load failure with the +// same tag `seedBucketsRun` itself fails with, without importing across command boundaries. +export { SeedConfigLoadError }; const CONFIG_PATH = "supabase/config.toml"; const UPLOAD_CONCURRENCY = 5; @@ -108,7 +111,12 @@ const decodeDefaultCliConfig = Schema.decodeUnknownSync(CliConfigSchema); * `emitSummary` gates the stdout summary; `interactive` (default `true`) gates overwrite/prune * prompts. The caller owns project-ref resolution, cache writes, and telemetry. */ -export const seedBucketsRun = Effect.fnUntraced(function* (opts: { +export const seedBucketsRun = Effect.fnUntraced(function* < + EA = never, + RA = never, + EC = never, + RC = never, +>(opts: { readonly projectRef: string; readonly emitSummary: boolean; readonly interactive?: boolean; @@ -120,7 +128,7 @@ export const seedBucketsRun = Effect.fnUntraced(function* (opts: { */ readonly yes?: boolean; /** - * Skips this function's own `loadCliConfig` reload in favor of a config the caller already + * Skips this function's own resolved config load in favor of a config the caller already * resolved through its own nested-env walk, so a fresh reload here can't drop an override * that exists only in the shell/dotenv, not in `config.toml`. */ @@ -149,6 +157,13 @@ export const seedBucketsRun = Effect.fnUntraced(function* (opts: { * would swallow the next line of a `curl … | bash` script. Declined prunes say so instead. */ readonly promptless?: boolean; + /** Announces the local Storage endpoint as the run's target once its URL is known. */ + readonly announceLocalEndpoint?: (baseUrl: string) => Effect.Effect; + /** + * Asks for consent to the hosted plan's writes before the first one. One answer covers every + * bucket overwrite and prune, so no per-bucket prompt follows it. + */ + readonly confirmHostedWrites?: (details: ReadonlyArray) => Effect.Effect; }) { const output = yield* Output; const cliSettings = yield* CommandSettings; @@ -156,44 +171,52 @@ export const seedBucketsRun = Effect.fnUntraced(function* (opts: { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const posixPath = yield* Effect.provide(Path.Path, BunPath.layerPosix); - const projectEnvValues = opts.projectEnvValues ?? (yield* loadProjectEnv(fs, path, workdir)); - // `--yes` OR `SUPABASE_YES`. - const yes = opts.yes ?? (yield* resolveYesWithProjectEnv(projectEnvValues)); const { projectRef, emitSummary } = opts; const interactive = opts.interactive ?? true; const promptless = opts.promptless ?? false; - // Loads config.toml, merging `[remotes.*]` overrides for `--linked`; skipped when the - // caller already supplied `resolvedConfig`. - // An explicit `opts.workdir` is the exact project root; only the caller's own workdir - // (`cliSettings.workdir`) may still search ancestors for `config.toml`. - const search = opts.workdir === undefined && shouldSearchAncestors(cliSettings); - const loadOptions: InternalLoadCliConfigOptions = - projectRef !== "" - ? { projectRef, goViperCompat: true, search } - : { goViperCompat: true, search }; - const loaded = + // Skipped when the caller already supplied `resolvedConfig`. A missing config file behaves as + // the embedded defaults, not an early exit: local + no-config falls into the no-op + // short-circuit below, while `--linked` + no-config still falls through to the remote path so + // auth/project/API failures surface. + const context = opts.resolvedConfig !== undefined - ? null - : yield* loadCliConfig(workdir, loadOptions).pipe( + ? undefined + : yield* loadResolvedConfigContext( + workdir, + projectRef === "" ? Option.none() : Option.some(projectRef), + ).pipe( Effect.catchTag( "CliConfigParseError", (cause) => new SeedConfigLoadError({ - message: `failed to parse ${relativeConfigPath(workdir, cause.path)}: ${configParseCauseMessage(cause.cause)}`, + message: describeConfigParseError(workdir, cause), }), ), + Effect.mapError((cause) => + cause instanceof SeedConfigLoadError || isConfigValueFailure(cause) + ? cause + : new SeedConfigLoadError({ message: describeConfigLoadFailure(cause) }), + ), ); - // A missing config file behaves as embedded defaults, not an early exit: local + no-config - // falls into the no-op short-circuit below, while `--linked` + no-config still falls - // through to the remote path so auth/project/API failures surface. - const config = - opts.resolvedConfig?.config ?? (loaded === null ? decodeDefaultCliConfig({}) : loaded.config); - const document = opts.resolvedConfig?.document ?? (loaded === null ? undefined : loaded.document); + const projectEnvValues = + opts.projectEnvValues ?? + context?.projectEnvValues ?? + (yield* loadCliProjectEnvFiles(workdir).pipe( + Effect.map((loaded) => loaded.values), + Effect.mapError((cause) => new SeedConfigLoadError({ message: cause.message })), + )); + // `--yes` OR `SUPABASE_YES`. + const yes = opts.yes ?? (yield* resolveYesWithProjectEnv(projectEnvValues)); + const config = opts.resolvedConfig?.config ?? context?.config ?? decodeDefaultCliConfig({}); + const document = opts.resolvedConfig?.document ?? context?.document; // Printed whenever a `[remotes.*]` block matched the linked ref; stderr in all output modes. - if (loaded !== null && loaded.appliedRemote !== undefined) { - yield* output.raw(`Loading config override: [remotes.${loaded.appliedRemote}]\n`, "stderr"); + const appliedRemote = Option.getOrUndefined( + context?.resolvedConfig.appliedRemote ?? Option.none(), + ); + if (appliedRemote !== undefined) { + yield* output.raw(`Loading config override: [remotes.${appliedRemote}]\n`, "stderr"); } const bucketsConfig = config.storage.buckets ?? {}; const bucketNames = Object.keys(bucketsConfig); @@ -227,7 +250,7 @@ export const seedBucketsRun = Effect.fnUntraced(function* (opts: { // consults these legacy-only inputs, so it skips this validation entirely. const backend = yield* currentStackBackend; if (backend.kind !== "stack") { - yield* validateLocalStorageConfig(config, projectEnvValues); + yield* validateLocalStorageConfig(); } if (emitSummary && output.format !== "text") { yield* output.success("", { ...emptySummary() }); @@ -236,13 +259,10 @@ export const seedBucketsRun = Effect.fnUntraced(function* (opts: { } // Build the Storage service-gateway client (local or remote). - const credentials = - opts.credentials ?? - (yield* resolveStorageCredentials({ - projectRef, - config, - projectEnvValues, - })); + const credentials = opts.credentials ?? (yield* resolveStorageCredentials({ projectRef })); + if (projectRef === "" && opts.announceLocalEndpoint !== undefined) { + yield* opts.announceLocalEndpoint(credentials.baseUrl); + } // Gateway operations use plain `globalThis.fetch` instead of the DoH fetch, with the Kong CA in // `FetchHttpClient.RequestInit` for local + https; the api-keys lookup in @@ -256,16 +276,36 @@ export const seedBucketsRun = Effect.fnUntraced(function* (opts: { const summary = emptySummary(); - yield* upsertBuckets(output, yes, interactive, promptless, gateway, bucketPropsByName, summary); + const existingBuckets = yield* gateway.listBuckets; + + // A hosted run never prompts per item: one consent covers every overwrite and prune. + const confirmHostedWrites = projectRef === "" ? undefined : opts.confirmHostedWrites; + if (confirmHostedWrites !== undefined) { + const details = yield* planHostedWrites(gateway, config, bucketPropsByName, existingBuckets); + if (details.length > 0) yield* confirmHostedWrites(details); + } + const approved = confirmHostedWrites !== undefined || yes; + const skipPrompts = confirmHostedWrites !== undefined || promptless; + + yield* upsertBuckets( + output, + approved, + interactive, + skipPrompts, + gateway, + existingBuckets, + bucketPropsByName, + summary, + ); // Upsert analytics buckets (remote --linked only). if (config.storage.analytics.enabled && projectRef !== "") { yield* output.raw("Updating analytics buckets...\n", "stderr"); yield* upsertAnalyticsBuckets( output, - yes, + approved, interactive, - promptless, + skipPrompts, gateway, Object.keys(config.storage.analytics.buckets), summary, @@ -277,9 +317,9 @@ export const seedBucketsRun = Effect.fnUntraced(function* (opts: { yield* output.raw("Updating vector buckets...\n", "stderr"); yield* upsertVectorBuckets( output, - yes, + approved, interactive, - promptless, + skipPrompts, gateway, vectorBucketNames, summary, @@ -336,6 +376,8 @@ const computeBucketProps = ( }), }); +type ExistingBucket = { readonly id: string; readonly name: string }; + // `propsByName` is precomputed and size-validated before this runs. const upsertBuckets = Effect.fnUntraced(function* ( output: typeof Output.Service, @@ -343,10 +385,10 @@ const upsertBuckets = Effect.fnUntraced(function* ( interactive: boolean, promptless: boolean, gateway: StorageGateway, + existing: ReadonlyArray, propsByName: ReadonlyMap, summary: SeedSummary, ) { - const existing = yield* gateway.listBuckets; const byName = new Map(existing.map((b) => [b.name, b.id])); for (const [name, props] of propsByName) { @@ -474,6 +516,71 @@ const upsertAnalyticsBuckets = Effect.fnUntraced(function* ( } }); +const joinNames = (names: ReadonlyArray): string => names.join(", "); + +/** + * The hosted writes `seedBucketsRun` is about to make, as consent detail lines; empty when it + * would write nothing. Reads the remote bucket lists but changes nothing. + */ +const planHostedWrites = Effect.fnUntraced(function* ( + gateway: StorageGateway, + config: CliConfig, + propsByName: ReadonlyMap, + existingBuckets: ReadonlyArray, +) { + const details: Array = []; + const existing = new Set(existingBuckets.map((bucket) => bucket.name)); + const configured = [...propsByName.keys()]; + const create = configured.filter((name) => !existing.has(name)); + const overwrite = configured.filter((name) => existing.has(name)); + if (create.length > 0) details.push(`Create buckets: ${joinNames(create)}`); + if (overwrite.length > 0) + details.push(`Overwrite the properties of buckets: ${joinNames(overwrite)}`); + + const withObjects = Object.entries(config.storage.buckets ?? {}) + .filter(([, bucket]) => bucket.objects_path.length > 0) + .map(([name]) => name); + if (withObjects.length > 0) { + details.push( + `Upload objects, overwriting existing ones, to buckets: ${joinNames(withObjects)}`, + ); + } + + if (config.storage.analytics.enabled) { + const declared = Object.keys(config.storage.analytics.buckets); + const present = new Set(yield* gateway.listAnalyticsBuckets); + const created = declared.filter((name) => !present.has(name)); + const pruned = [...present].filter((name) => !declared.includes(name)); + if (created.length > 0) details.push(`Create analytics buckets: ${joinNames(created)}`); + if (pruned.length > 0) { + details.push(`Delete analytics buckets not declared in ${CONFIG_PATH}: ${joinNames(pruned)}`); + } + } + + const declaredVector = Object.keys(config.storage.vector.buckets); + if (config.storage.vector.enabled && declaredVector.length > 0) { + const listed = yield* gateway.listVectorBuckets.pipe( + Effect.map(Option.some), + Effect.catch((error) => + isVectorBucketsFeatureNotEnabled(error.message) || + isLocalVectorBucketsUnavailable(error.message) + ? Effect.succeed(Option.none>()) + : Effect.fail(error), + ), + ); + if (Option.isSome(listed)) { + const present = new Set(listed.value); + const created = declaredVector.filter((name) => !present.has(name)); + const pruned = listed.value.filter((name) => !declaredVector.includes(name)); + if (created.length > 0) details.push(`Create vector buckets: ${joinNames(created)}`); + if (pruned.length > 0) { + details.push(`Delete vector buckets not declared in ${CONFIG_PATH}: ${joinNames(pruned)}`); + } + } + } + return details; +}); + /** * On `FeatureNotEnabled` or local-unavailable errors, prints the matching warning and * continues (object upload still runs); any other error propagates. diff --git a/apps/cli/src/command-internal/seed-ops.unit.test.ts b/apps/cli/src/command-internal/seed-ops.unit.test.ts index 6fc7a4859a..0b9c118b8f 100644 --- a/apps/cli/src/command-internal/seed-ops.unit.test.ts +++ b/apps/cli/src/command-internal/seed-ops.unit.test.ts @@ -44,22 +44,20 @@ const tempDir = (prefix: string) => // (`../../../shared/path-match.ts`) end to end; pattern-matching semantics are covered by // `path-match.unit.test.ts`. layer(BunServices.layer)("getPendingSeeds (glob character classes)", (it) => { - it.effect( - "treats a leading `!` in a bracket class as literal, not negation (Go path.Match parity)", - () => - Effect.gen(function* () { - const { fs, path, dir } = yield* tempDir("seed-glob-"); - yield* fs.writeFileString(path.join(dir, "a.sql"), "select 1;"); - yield* fs.writeFileString(path.join(dir, "b.sql"), "select 2;"); - const { session } = fakeSeedSession(); - // `[!a]` is a positive class of `!` and `a`; only a leading `^` negates. - const pending = yield* getPendingSeeds(session, fs, path, ["[!a].sql"], dir); - expect(pending.map((seed) => seed.path)).toEqual(["a.sql"]); - }).pipe(Effect.provide(mockOutput({ format: "text" }).layer)), + it.effect("treats a leading `!` in a bracket class as literal, not negation", () => + Effect.gen(function* () { + const { fs, path, dir } = yield* tempDir("seed-glob-"); + yield* fs.writeFileString(path.join(dir, "a.sql"), "select 1;"); + yield* fs.writeFileString(path.join(dir, "b.sql"), "select 2;"); + const { session } = fakeSeedSession(); + // `[!a]` is a positive class of `!` and `a`; only a leading `^` negates. + const pending = yield* getPendingSeeds(session, fs, path, ["[!a].sql"], dir); + expect(pending.map((seed) => seed.path)).toEqual(["a.sql"]); + }).pipe(Effect.provide(mockOutput({ format: "text" }).layer)), ); it.effect( - "warns Go's bad-pattern message for an unterminated bracket class, not a bogus no-match", + "warns the bad-pattern message for an unterminated bracket class, not a bogus no-match", () => { // An unclosed `[` is malformed, so `fs.Glob` reports a syntax error, not a bogus // "no files matched" for a well-formed-but-empty pattern. @@ -109,7 +107,7 @@ layer(BunServices.layer)("seedData (dirty parse)", (it) => { ); it.effect( - "rejects an oversized seed statement when SUPABASE_SCANNER_BUFFER_SIZE is configured (Go SeedFile.ExecBatchWithCache parity)", + "rejects an oversized seed statement when SUPABASE_SCANNER_BUFFER_SIZE is configured", () => Effect.gen(function* () { const { fs, path, dir } = yield* tempDir("seed-scanner-"); @@ -121,7 +119,7 @@ layer(BunServices.layer)("seedData (dirty parse)", (it) => { { path: "big.sql", hash: "newhash", dirty: false }, ]).pipe(Effect.flip); expect(error.message).toBe( - "bufio.Scanner: token too long\nAfter statement 0: \nTry setting SUPABASE_SCANNER_BUFFER_SIZE=5MB (current size is 0KB)", + "scanner: token too long\nAfter statement 0: \nTry setting SUPABASE_SCANNER_BUFFER_SIZE=5MB (current size is 0KB)", ); expect(calls.some((c) => c.sql.includes("select"))).toBe(false); }).pipe((body) => withConfigEnv({ SUPABASE_SCANNER_BUFFER_SIZE: "100b" }, body)), diff --git a/apps/cli/src/command-internal/seed-remote-consent.integration.test.ts b/apps/cli/src/command-internal/seed-remote-consent.integration.test.ts new file mode 100644 index 0000000000..0ef8cc2bf7 --- /dev/null +++ b/apps/cli/src/command-internal/seed-remote-consent.integration.test.ts @@ -0,0 +1,96 @@ +import { BunServices } from "@effect/platform-bun"; +import { describe, expect, it } from "@effect/vitest"; +import { Effect, FileSystem, Layer, Option, Path } from "effect"; + +import { configValuesLayer } from "../../tests/helpers/config-values-layer.ts"; +import { mockCommandSettings } from "../../tests/helpers/command-mocks.ts"; +import { CliConfigValues } from "../config/cli-config-values.service.ts"; +import { planDbSeed } from "./seed-remote-consent.ts"; + +const REF = "abcdefghijklmnopqrst"; + +const plan = ( + toml: string, + options: { + readonly projectRef?: Option.Option; + readonly env?: Record; + } = {}, +) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const workdir = yield* fs.makeTempDirectoryScoped({ prefix: "seed-plan-" }); + yield* fs.makeDirectory(path.join(workdir, "supabase"), { recursive: true }); + yield* fs.writeFileString(path.join(workdir, "supabase", "config.toml"), toml); + const configValues = yield* CliConfigValues; + const resolved = yield* configValues.load({ + workdir, + projectRef: options.projectRef ?? Option.some(REF), + }); + return yield* planDbSeed(resolved, { workdir, ref: REF }); + }).pipe( + Effect.scoped, + Effect.provide( + Layer.mergeAll( + configValuesLayer({ env: options.env }), + mockCommandSettings({ workdir: "/unused" }), + BunServices.layer, + ), + ), + ); + +const FILES = ["supabase/seed.sql", "supabase/seed_users.sql"]; + +describe("planDbSeed", () => { + it.effect("describes the files without naming a remote when none matched", () => + Effect.gen(function* () { + const { input, detail } = yield* plan("", { projectRef: Option.none() }); + expect(input.consent).toBeUndefined(); + expect(detail(FILES)).toBe("seed 2 files"); + expect(detail(FILES.slice(0, 1))).toBe("seed 1 file"); + }), + ); + + it.effect("names the matched remote and what turned seeding on", () => + Effect.gen(function* () { + const { input, detail } = yield* plan(`[remotes.prod]\nproject_id = "${REF}"\n`, { + env: { SUPABASE_DB_SEED_ENABLED: "true" }, + }); + expect(input.enabled).toBe(true); + expect(input.consent).toMatchObject({ remote: "prod", ref: REF }); + expect(input.consent?.enabledBy).toContain("SUPABASE_DB_SEED_ENABLED"); + expect(detail(FILES)).toBe( + `seed 2 files into [remotes.prod] (seeding enabled by ${input.consent?.enabledBy})`, + ); + }), + ); + + it.effect("omits the origin when the remote enables seeding itself", () => + Effect.gen(function* () { + const { input, detail } = yield* plan( + `[remotes.prod]\nproject_id = "${REF}"\n[remotes.prod.db.seed]\nenabled = true\n`, + ); + expect(input.consent).toBeUndefined(); + expect(detail(FILES)).toBe("seed 2 files into [remotes.prod]"); + }), + ); + + it.effect("has no detail when a matched remote leaves seeding off", () => + Effect.gen(function* () { + const { input, detail } = yield* plan(`[remotes.prod]\nproject_id = "${REF}"\n`); + expect(input.enabled).toBe(false); + expect(detail(FILES)).toBeUndefined(); + }), + ); + + it.effect("has no detail when seeding is off or there is nothing to seed", () => + Effect.gen(function* () { + const off = yield* plan("[db.seed]\nenabled = false\n"); + expect(off.input.enabled).toBe(false); + expect(off.detail(FILES)).toBeUndefined(); + + const none = yield* plan(""); + expect(none.detail([])).toBeUndefined(); + }), + ); +}); diff --git a/apps/cli/src/command-internal/seed-remote-consent.ts b/apps/cli/src/command-internal/seed-remote-consent.ts new file mode 100644 index 0000000000..70b6a60ecc --- /dev/null +++ b/apps/cli/src/command-internal/seed-remote-consent.ts @@ -0,0 +1,68 @@ +import { Effect, Option, Path, Predicate } from "effect"; + +import { CliConfigKeys } from "../config/cli-config-keys.ts"; +import { describeCliConfigOrigin } from "../config/cli-config-key.ts"; +import type { ResolvedCliConfig } from "../config/cli-config-values.service.ts"; + +/** A seed run whose target matched a `[remotes.*]` block that did not turn seeding on itself. */ +interface SeedConsentTarget { + readonly ref: string; + readonly remote: string; + /** What turned seeding on, as `describeCliConfigOrigin` words it. */ + readonly enabledBy: string; +} + +/** The effective `[db.seed]` values a command acts on, plus the remote that did not enable them. */ +interface DbSeedInput { + readonly enabled: boolean; + readonly sqlPaths: ReadonlyArray; + readonly consent: SeedConsentTarget | undefined; +} + +/** The seed input plus the line a hosted-mutation consent shows for the files a run would seed. */ +export interface DbSeedPlan { + readonly input: DbSeedInput; + /** `undefined` when seeding is off or `files` is empty. */ + readonly detail: (files: ReadonlyArray) => string | undefined; +} + +const remoteDeclaresSeedEnabled = (resolvedConfig: ResolvedCliConfig, remote: string): boolean => { + const block = resolvedConfig.loaded.interpolatedRemotes?.[remote]; + const declared = + Predicate.hasProperty(block, "db") && + Predicate.hasProperty(block.db, "seed") && + Predicate.hasProperty(block.db.seed, "enabled") + ? block.db.seed.enabled + : undefined; + return CliConfigKeys.db.seed.enabled.codec.fromConfig(declared) === true; +}; + +/** Reads `[db.seed]` from the resolved config; one consent carries {@link DbSeedPlan.detail}. */ +export const planDbSeed = Effect.fn("DbSeedPlan.plan")(function* ( + resolvedConfig: ResolvedCliConfig, + target: { readonly workdir: string; readonly ref: string }, +) { + const path = yield* Path.Path; + const enabled = yield* resolvedConfig.get(CliConfigKeys.db.seed.enabled); + const sqlPaths = yield* resolvedConfig.get(CliConfigKeys.db.seed.sqlPaths); + const remote = Option.getOrUndefined(resolvedConfig.appliedRemote); + const consent: SeedConsentTarget | undefined = + remote === undefined || remoteDeclaresSeedEnabled(resolvedConfig, remote) + ? undefined + : { + ref: target.ref, + remote, + enabledBy: describeCliConfigOrigin(enabled.origin, { workdir: target.workdir, path }), + }; + const input: DbSeedInput = { enabled: enabled.value, sqlPaths: sqlPaths.value, consent }; + return { + input, + detail: (files) => { + if (!input.enabled || files.length === 0) return undefined; + const noun = files.length === 1 ? "file" : "files"; + const into = remote === undefined ? "" : ` into [remotes.${remote}]`; + const enabledBy = consent === undefined ? "" : ` (seeding enabled by ${consent.enabledBy})`; + return `seed ${files.length} ${noun}${into}${enabledBy}`; + }, + } satisfies DbSeedPlan; +}); diff --git a/apps/cli/src/command-internal/seed.unit.test.ts b/apps/cli/src/command-internal/seed.unit.test.ts index 38c439bf5d..f15a8ff9cc 100644 --- a/apps/cli/src/command-internal/seed.unit.test.ts +++ b/apps/cli/src/command-internal/seed.unit.test.ts @@ -76,33 +76,31 @@ layer(BunServices.layer)("applySeedFiles seed glob", (it) => { }), ); - it.effect( - "expands a matched directory to its sorted, regular .sql files (Go's Glob.SQLFiles)", - () => - Effect.gen(function* () { - const { fs, path, dir } = yield* tempDir("seed-"); - yield* fs.makeDirectory(path.join(dir, "seeds")); - yield* fs.writeFileString(path.join(dir, "seeds", "b.sql"), "insert into t values (2);"); - yield* fs.writeFileString(path.join(dir, "seeds", "a.sql"), "insert into t values (1);"); - yield* fs.writeFileString(path.join(dir, "seeds", "README.md"), "not a seed file"); - const { session, queries } = fakeSession(); - const out = mockOutput(); - yield* run(session, dir, ["seeds"], out); - const upserts = queries.filter((q) => - q.sql.includes("INSERT INTO supabase_migrations.seed_files"), - ); - expect(upserts.map((q) => q.params?.[0])).toEqual(["seeds/a.sql", "seeds/b.sql"]); - expect(out.rawChunks.map((c) => c.text)).toEqual([ - "Seeding data from seeds/a.sql...\n", - "Seeding data from seeds/b.sql...\n", - ]); - }), + it.effect("expands a matched directory to its sorted, regular .sql files", () => + Effect.gen(function* () { + const { fs, path, dir } = yield* tempDir("seed-"); + yield* fs.makeDirectory(path.join(dir, "seeds")); + yield* fs.writeFileString(path.join(dir, "seeds", "b.sql"), "insert into t values (2);"); + yield* fs.writeFileString(path.join(dir, "seeds", "a.sql"), "insert into t values (1);"); + yield* fs.writeFileString(path.join(dir, "seeds", "README.md"), "not a seed file"); + const { session, queries } = fakeSession(); + const out = mockOutput(); + yield* run(session, dir, ["seeds"], out); + const upserts = queries.filter((q) => + q.sql.includes("INSERT INTO supabase_migrations.seed_files"), + ); + expect(upserts.map((q) => q.params?.[0])).toEqual(["seeds/a.sql", "seeds/b.sql"]); + expect(out.rawChunks.map((c) => c.text)).toEqual([ + "Seeding data from seeds/a.sql...\n", + "Seeding data from seeds/b.sql...\n", + ]); + }), ); }); layer(BunServices.layer)("applySeedFiles scanner buffer size", (it) => { it.effect( - "rejects an oversized seed statement when SUPABASE_SCANNER_BUFFER_SIZE is configured (Go SeedFile.ExecBatchWithCache parity)", + "rejects an oversized seed statement when SUPABASE_SCANNER_BUFFER_SIZE is configured", () => Effect.gen(function* () { const { fs, path, dir } = yield* tempDir("seed-scanner-"); @@ -116,7 +114,7 @@ layer(BunServices.layer)("applySeedFiles scanner buffer size", (it) => { const error = yield* run(session, dir, ["big.sql"], mockOutput()).pipe(Effect.flip); expect(error).toBeInstanceOf(MigrationSeedError); expect(error.message).toBe( - "bufio.Scanner: token too long\nAfter statement 0: \nTry setting SUPABASE_SCANNER_BUFFER_SIZE=5MB (current size is 0KB)", + "scanner: token too long\nAfter statement 0: \nTry setting SUPABASE_SCANNER_BUFFER_SIZE=5MB (current size is 0KB)", ); expect(queries.some((q) => q.sql.includes("insert into t"))).toBe(false); }).pipe((body) => withConfigEnv({ SUPABASE_SCANNER_BUFFER_SIZE: "100b" }, body)), diff --git a/apps/cli/src/command-internal/service-version-overrides.ts b/apps/cli/src/command-internal/service-version-overrides.ts index 19ba71b4d9..20496b3f3e 100644 --- a/apps/cli/src/command-internal/service-version-overrides.ts +++ b/apps/cli/src/command-internal/service-version-overrides.ts @@ -8,7 +8,10 @@ import { type CliErrorActionabilityDeclaration, ErrorActionabilityId, } from "../shared/telemetry/error-actionability.ts"; -import { legacyProjectStatePaths, type LegacyStateFile } from "./legacy-project-state.paths.ts"; +import { + legacyProjectStatePaths, + type LegacyStateFile, +} from "../shared/config/legacy-project-state.paths.ts"; import { usesStackState } from "./stack-backend.ts"; /** A saved `.temp` pin is not a usable image tag. The file is left unchanged. */ diff --git a/apps/cli/src/command-internal/size-units.ts b/apps/cli/src/command-internal/size-units.ts index e8af1e3feb..17317229b4 100644 --- a/apps/cli/src/command-internal/size-units.ts +++ b/apps/cli/src/command-internal/size-units.ts @@ -1,9 +1,7 @@ /** - * Parses `file_size_limit` config values using `docker/go-units`' `RAMInBytes` grammar - * (1024-based, case-insensitive, optional trailing `b`) before sending them to service APIs. + * Parses `file_size_limit` config values (1024-based, case-insensitive, optional trailing `b`) + * before sending them to service APIs. * Shared by `config push` (storage/auth/api/db diffing) and `seed buckets`. - * - * @see github.com/docker/go-units@v0.5.0/size.go */ const BINARY_MAP: Readonly> = { @@ -48,7 +46,7 @@ export function ramInBytes(sizeStr: string): number { throw new Error(`invalid size: '${sizeStr}'`); } // Strip the (already-validated, between-digits) underscores before parsing: - // JS `Number.parseFloat("1_000")` stops at the underscore (→1), unlike Go. + // JS `Number.parseFloat("1_000")` stops at the underscore (→1). const size = Number.parseFloat(num.replace(/_/g, "")); // Reject NaN and ±Infinity: an overflowing numeral like `1e309` parses to Infinity in JS, // but must fail here rather than flow through as `null` in the request body. diff --git a/apps/cli/src/command-internal/smtp-enabled.integration.test.ts b/apps/cli/src/command-internal/smtp-enabled.integration.test.ts new file mode 100644 index 0000000000..de2d1710dc --- /dev/null +++ b/apps/cli/src/command-internal/smtp-enabled.integration.test.ts @@ -0,0 +1,50 @@ +import { BunServices } from "@effect/platform-bun"; +import { describe, expect, it } from "@effect/vitest"; +import { Effect, Option } from "effect"; + +import { withConfigEnv } from "../../tests/helpers/command-mocks.ts"; +import { definedEnv } from "../../tests/helpers/config-env-pins.ts"; +import { configValuesLayer } from "../../tests/helpers/config-values-layer.ts"; +import { createStackConfigProject } from "../../tests/helpers/stack-config.ts"; +import { CliConfigValues } from "../config/cli-config-values.service.ts"; +import { resolveSmtpEnabled } from "./smtp-enabled.ts"; + +const smtpEnabled = (config: string, shell: Record = {}) => + Effect.gen(function* () { + const root = yield* createStackConfigProject(config, { prefix: "supabase-smtp-enabled-" }); + const resolvedConfig = yield* CliConfigValues.use((values) => + values.load({ workdir: root, projectRef: Option.none() }), + ).pipe(Effect.provide(configValuesLayer())); + return resolveSmtpEnabled(resolvedConfig); + }).pipe( + Effect.provide(BunServices.layer), + (effect) => withConfigEnv(definedEnv(shell), effect), + Effect.scoped, + ); + +describe("resolveSmtpEnabled", () => { + it.live("is off when [auth.email.smtp] is absent", () => + Effect.gen(function* () { + expect(yield* smtpEnabled('project_id = "p"\n')).toBe(false); + }), + ); + + it.live("is on when the table is present and leaves enabled out", () => + Effect.gen(function* () { + expect(yield* smtpEnabled('[auth.email.smtp]\nhost = "smtp.test"\n')).toBe(true); + }), + ); + + it.live("follows an explicit enabled = false", () => + Effect.gen(function* () { + expect(yield* smtpEnabled("[auth.email.smtp]\nenabled = false\n")).toBe(false); + }), + ); + + it.live("follows an env override of enabled over a present table", () => + Effect.gen(function* () { + const config = "[auth.email.smtp]\nenabled = true\n"; + expect(yield* smtpEnabled(config, { SUPABASE_AUTH_EMAIL_SMTP_ENABLED: "false" })).toBe(false); + }), + ); +}); diff --git a/apps/cli/src/command-internal/smtp-enabled.ts b/apps/cli/src/command-internal/smtp-enabled.ts new file mode 100644 index 0000000000..4ab93b7f0b --- /dev/null +++ b/apps/cli/src/command-internal/smtp-enabled.ts @@ -0,0 +1,7 @@ +import type { ResolvedCliConfig } from "../config/cli-config-values.service.ts"; + +/** A present `[auth.email.smtp]` table without `enabled` counts as on; the schema default is off. */ +export const resolveSmtpEnabled = (resolvedConfig: ResolvedCliConfig): boolean => + resolvedConfig.materialized.originAt("auth.email.smtp.enabled").tier === "default" + ? resolvedConfig.declares("auth.email.smtp") + : resolvedConfig.materialized.config.auth.email.smtp?.enabled === true; diff --git a/apps/cli/src/command-internal/sql-files-glob.unit.test.ts b/apps/cli/src/command-internal/sql-files-glob.unit.test.ts index 2329ad9858..b8795a1d16 100644 --- a/apps/cli/src/command-internal/sql-files-glob.unit.test.ts +++ b/apps/cli/src/command-internal/sql-files-glob.unit.test.ts @@ -46,7 +46,7 @@ const withMode = (target: string, mode: number, body: Effect.Effect { - it.effect("treats an empty pattern as no match, not the workdir itself (Go fs.Glob parity)", () => + it.effect("treats an empty pattern as no match, not the workdir itself", () => Effect.gen(function* () { const { dir } = yield* tempWorkdir("sql-glob-empty-"); const result = yield* run([""], dir); @@ -55,44 +55,40 @@ layer(BunServices.layer)("sqlFilesGlob", (it) => { }), ); - it.effect( - "does not follow a symlinked .sql file below a matched directory (Go WalkDir parity)", - () => - Effect.gen(function* () { - const { fs, path, dir } = yield* tempWorkdir("sql-glob-symlink-file-"); - const schemasDir = path.join(dir, "schemas"); - yield* fs.makeDirectory(schemasDir); - yield* fs.writeFileString(path.join(schemasDir, "real.sql"), "select 1;"); - const outsideDir = path.join(dir, "outside"); - yield* fs.makeDirectory(outsideDir); - yield* fs.writeFileString(path.join(outsideDir, "evil.sql"), "select 2;"); - yield* fs.symlink(path.join(outsideDir, "evil.sql"), path.join(schemasDir, "linked.sql")); - const result = yield* run(["schemas"], dir); - expect(result.files).toEqual(["schemas/real.sql"]); - expect(result.warnings).toEqual([]); - }), + it.effect("does not follow a symlinked .sql file below a matched directory", () => + Effect.gen(function* () { + const { fs, path, dir } = yield* tempWorkdir("sql-glob-symlink-file-"); + const schemasDir = path.join(dir, "schemas"); + yield* fs.makeDirectory(schemasDir); + yield* fs.writeFileString(path.join(schemasDir, "real.sql"), "select 1;"); + const outsideDir = path.join(dir, "outside"); + yield* fs.makeDirectory(outsideDir); + yield* fs.writeFileString(path.join(outsideDir, "evil.sql"), "select 2;"); + yield* fs.symlink(path.join(outsideDir, "evil.sql"), path.join(schemasDir, "linked.sql")); + const result = yield* run(["schemas"], dir); + expect(result.files).toEqual(["schemas/real.sql"]); + expect(result.warnings).toEqual([]); + }), ); - it.effect( - "does not recurse into a symlinked subdirectory below a matched directory (Go WalkDir parity)", - () => - Effect.gen(function* () { - const { fs, path, dir } = yield* tempWorkdir("sql-glob-symlink-dir-"); - const schemasDir = path.join(dir, "schemas"); - yield* fs.makeDirectory(schemasDir); - yield* fs.writeFileString(path.join(schemasDir, "real.sql"), "select 1;"); - const outsideSubdir = path.join(dir, "outside-subdir"); - yield* fs.makeDirectory(outsideSubdir); - yield* fs.writeFileString(path.join(outsideSubdir, "nested.sql"), "select 3;"); - yield* fs.symlink(outsideSubdir, path.join(schemasDir, "linked-dir")); - const result = yield* run(["schemas"], dir); - expect(result.files).toEqual(["schemas/real.sql"]); - expect(result.warnings).toEqual([]); - }), + it.effect("does not recurse into a symlinked subdirectory below a matched directory", () => + Effect.gen(function* () { + const { fs, path, dir } = yield* tempWorkdir("sql-glob-symlink-dir-"); + const schemasDir = path.join(dir, "schemas"); + yield* fs.makeDirectory(schemasDir); + yield* fs.writeFileString(path.join(schemasDir, "real.sql"), "select 1;"); + const outsideSubdir = path.join(dir, "outside-subdir"); + yield* fs.makeDirectory(outsideSubdir); + yield* fs.writeFileString(path.join(outsideSubdir, "nested.sql"), "select 3;"); + yield* fs.symlink(outsideSubdir, path.join(schemasDir, "linked-dir")); + const result = yield* run(["schemas"], dir); + expect(result.files).toEqual(["schemas/real.sql"]); + expect(result.warnings).toEqual([]); + }), ); it.effect( - "surfaces a stat failure on a matched file as a warning instead of treating it as a regular file (Go parity)", + "surfaces a stat failure on a matched file as a warning instead of treating it as a regular file", () => Effect.gen(function* () { const { fs, path, dir } = yield* tempWorkdir("sql-glob-stat-fail-"); @@ -113,7 +109,7 @@ layer(BunServices.layer)("sqlFilesGlob", (it) => { ); it.effect( - "surfaces a stat failure for a LITERAL (no-metacharacter) pattern naming a broken symlink, instead of reporting no match (Go afero.Glob Lstat parity)", + "surfaces a stat failure for a LITERAL (no-metacharacter) pattern naming a broken symlink, instead of reporting no match", () => Effect.gen(function* () { const { fs, path, dir } = yield* tempWorkdir("sql-glob-literal-symlink-"); @@ -132,7 +128,7 @@ layer(BunServices.layer)("sqlFilesGlob", (it) => { ); it.effect( - "keeps a bare root ('/') as the directory when a glob pattern's meta character is in the first path component (Go afero.Glob parity)", + "keeps a bare root ('/') as the directory when a glob pattern's meta character is in the first path component", () => Effect.gen(function* () { // The canary file lives in the workdir (never the real "/"), proving this doesn't @@ -148,7 +144,7 @@ layer(BunServices.layer)("sqlFilesGlob", (it) => { ); it.effect( - "recurses through a root-anchored directory component without falling back to the workdir (Go afero.Glob parity)", + "recurses through a root-anchored directory component without falling back to the workdir", () => Effect.gen(function* () { // The workdir here contains a subdirectory that would match "foo*" only if the @@ -166,7 +162,7 @@ layer(BunServices.layer)("sqlFilesGlob", (it) => { ); it.effect( - "preserves a Windows drive root ('C:/') as the directory when splitting a glob pattern (Go filepath.Split parity)", + "preserves a Windows drive root ('C:/') as the directory when splitting a glob pattern", () => Effect.gen(function* () { const { fs, path, dir } = yield* tempWorkdir("sql-glob-drive-root-"); @@ -190,7 +186,7 @@ layer(BunServices.layer)("sqlFilesGlob", (it) => { ); it.effect( - "reports the raw backslash pattern in a 'no files matched' warning on Windows, not the slashed form used for matching (Go filepath.ToSlash parity)", + "reports the raw backslash pattern in a 'no files matched' warning on Windows, not the slashed form used for matching", () => Effect.gen(function* () { const { fs, dir } = yield* tempWorkdir("sql-glob-win-warn-"); @@ -202,41 +198,39 @@ layer(BunServices.layer)("sqlFilesGlob", (it) => { }), ); - it.effect( - "sorts raw backslash-joined Windows matches BEFORE slashing, not after (Go afero.Glob/filepath.ToSlash ordering parity)", - () => - Effect.gen(function* () { - // For `a\x.sql` vs `a0\x.sql`, sorting the raw backslash bytes puts `a0\x.sql` first - // (`\` is `0x5C` > `0`'s `0x30`), while sorting the slashed form would put `a/x.sql` - // first instead — a fully faked filesystem supplies canned results keyed by the exact - // backslash-joined paths `BunPath.layerWin32`'s `path.join` computes. - const { fs, path, dir } = yield* tempWorkdir("sql-glob-win-sort-"); - const canaryFile = path.join(dir, "canary.sql"); - yield* fs.writeFileString(canaryFile, "select 1;"); - const fileInfo = yield* fs.stat(canaryFile); - const workdir = "/workdir"; - const winFs: FileSystem.FileSystem = { - ...fs, - readDirectory: (p: string) => { - if (p === workdir) return Effect.succeed(["a", "a0"]); - if (p === "\\workdir\\a" || p === "\\workdir\\a0") return Effect.succeed(["x.sql"]); - return fs.readDirectory(p); - }, - stat: (p: string) => - p === "\\workdir\\a\\x.sql" || p === "\\workdir\\a0\\x.sql" - ? Effect.succeed(fileInfo) - : fs.stat(p), - }; - const result = yield* withWin32Path((win32Path) => - sqlFilesGlob(winFs, win32Path, ["a*/x.sql"], workdir), - ); - expect(result.files).toEqual(["a0/x.sql", "a/x.sql"]); - expect(result.warnings).toEqual([]); - }), + it.effect("sorts raw backslash-joined Windows matches BEFORE slashing, not after", () => + Effect.gen(function* () { + // For `a\x.sql` vs `a0\x.sql`, sorting the raw backslash bytes puts `a0\x.sql` first + // (`\` is `0x5C` > `0`'s `0x30`), while sorting the slashed form would put `a/x.sql` + // first instead — a fully faked filesystem supplies canned results keyed by the exact + // backslash-joined paths `BunPath.layerWin32`'s `path.join` computes. + const { fs, path, dir } = yield* tempWorkdir("sql-glob-win-sort-"); + const canaryFile = path.join(dir, "canary.sql"); + yield* fs.writeFileString(canaryFile, "select 1;"); + const fileInfo = yield* fs.stat(canaryFile); + const workdir = "/workdir"; + const winFs: FileSystem.FileSystem = { + ...fs, + readDirectory: (p: string) => { + if (p === workdir) return Effect.succeed(["a", "a0"]); + if (p === "\\workdir\\a" || p === "\\workdir\\a0") return Effect.succeed(["x.sql"]); + return fs.readDirectory(p); + }, + stat: (p: string) => + p === "\\workdir\\a\\x.sql" || p === "\\workdir\\a0\\x.sql" + ? Effect.succeed(fileInfo) + : fs.stat(p), + }; + const result = yield* withWin32Path((win32Path) => + sqlFilesGlob(winFs, win32Path, ["a*/x.sql"], workdir), + ); + expect(result.files).toEqual(["a0/x.sql", "a/x.sql"]); + expect(result.warnings).toEqual([]); + }), ); it.effect( - "normalizes a doubled slash when the matched directory itself has a trailing slash (Go path.Join parity)", + "normalizes a doubled slash when the matched directory itself has a trailing slash", () => Effect.gen(function* () { const { fs, path, dir } = yield* tempWorkdir("sql-glob-trailing-slash-"); @@ -249,69 +243,61 @@ layer(BunServices.layer)("sqlFilesGlob", (it) => { }), ); - it.effect( - "drops the './' prefix when the matched directory cleans to '.' (Go path.Join parity)", - () => - Effect.gen(function* () { - const { fs, path, dir } = yield* tempWorkdir("sql-glob-dot-root-"); - yield* fs.writeFileString(path.join(dir, "a.sql"), "select 1;"); - const nestedDir = path.join(dir, "nested"); - yield* fs.makeDirectory(nestedDir); - yield* fs.writeFileString(path.join(nestedDir, "b.sql"), "select 2;"); - const result = yield* run(["."], dir); - expect(result.files).toEqual(["a.sql", "nested/b.sql"]); - expect(result.warnings).toEqual([]); - }), + it.effect("drops the './' prefix when the matched directory cleans to '.'", () => + Effect.gen(function* () { + const { fs, path, dir } = yield* tempWorkdir("sql-glob-dot-root-"); + yield* fs.writeFileString(path.join(dir, "a.sql"), "select 1;"); + const nestedDir = path.join(dir, "nested"); + yield* fs.makeDirectory(nestedDir); + yield* fs.writeFileString(path.join(nestedDir, "b.sql"), "select 2;"); + const result = yield* run(["."], dir); + expect(result.files).toEqual(["a.sql", "nested/b.sql"]); + expect(result.warnings).toEqual([]); + }), ); - it.effect( - "cleans a '..'-segment matched directory when walking its children (Go path.Join parity)", - () => - Effect.gen(function* () { - const { fs, path, dir } = yield* tempWorkdir("sql-glob-dotdot-segment-"); - const schemasDir = path.join(dir, "schemas"); - yield* fs.makeDirectory(schemasDir); - yield* fs.writeFileString(path.join(schemasDir, "a.sql"), "select 1;"); - const result = yield* run(["nested/../schemas"], dir); - expect(result.files).toEqual(["schemas/a.sql"]); - expect(result.warnings).toEqual([]); - }), + it.effect("cleans a '..'-segment matched directory when walking its children", () => + Effect.gen(function* () { + const { fs, path, dir } = yield* tempWorkdir("sql-glob-dotdot-segment-"); + const schemasDir = path.join(dir, "schemas"); + yield* fs.makeDirectory(schemasDir); + yield* fs.writeFileString(path.join(schemasDir, "a.sql"), "select 1;"); + const result = yield* run(["nested/../schemas"], dir); + expect(result.files).toEqual(["schemas/a.sql"]); + expect(result.warnings).toEqual([]); + }), ); - it.effect( - "cleans a direct glob match whose directory portion has a '.' segment (Go afero.Glob parity)", - () => - Effect.gen(function* () { - // Distinct from the walked-child cleaning tests above: the glob metacharacter is in - // the final component, so this matches directly via `globOne`, never `walkSqlFiles`. - const { fs, path, dir } = yield* tempWorkdir("sql-glob-direct-dot-segment-"); - const schemasDir = path.join(dir, "schemas"); - yield* fs.makeDirectory(schemasDir); - yield* fs.writeFileString(path.join(schemasDir, "a.sql"), "select 1;"); - const result = yield* run(["schemas/./*.sql"], dir); - expect(result.files).toEqual(["schemas/a.sql"]); - expect(result.warnings).toEqual([]); - }), + it.effect("cleans a direct glob match whose directory portion has a '.' segment", () => + Effect.gen(function* () { + // Distinct from the walked-child cleaning tests above: the glob metacharacter is in + // the final component, so this matches directly via `globOne`, never `walkSqlFiles`. + const { fs, path, dir } = yield* tempWorkdir("sql-glob-direct-dot-segment-"); + const schemasDir = path.join(dir, "schemas"); + yield* fs.makeDirectory(schemasDir); + yield* fs.writeFileString(path.join(schemasDir, "a.sql"), "select 1;"); + const result = yield* run(["schemas/./*.sql"], dir); + expect(result.files).toEqual(["schemas/a.sql"]); + expect(result.warnings).toEqual([]); + }), ); - it.effect( - "cleans a direct glob match whose directory portion has a '..' segment (Go afero.Glob parity)", - () => - Effect.gen(function* () { - // Same distinction as above (a direct match via `globOne`), but for an embedded `..` - // segment instead of `.`. - const { fs, path, dir } = yield* tempWorkdir("sql-glob-direct-dotdot-segment-"); - const schemasDir = path.join(dir, "schemas"); - yield* fs.makeDirectory(schemasDir); - yield* fs.writeFileString(path.join(schemasDir, "a.sql"), "select 1;"); - const result = yield* run(["nested/../schemas/*.sql"], dir); - expect(result.files).toEqual(["schemas/a.sql"]); - expect(result.warnings).toEqual([]); - }), + it.effect("cleans a direct glob match whose directory portion has a '..' segment", () => + Effect.gen(function* () { + // Same distinction as above (a direct match via `globOne`), but for an embedded `..` + // segment instead of `.`. + const { fs, path, dir } = yield* tempWorkdir("sql-glob-direct-dotdot-segment-"); + const schemasDir = path.join(dir, "schemas"); + yield* fs.makeDirectory(schemasDir); + yield* fs.writeFileString(path.join(schemasDir, "a.sql"), "select 1;"); + const result = yield* run(["nested/../schemas/*.sql"], dir); + expect(result.files).toEqual(["schemas/a.sql"]); + expect(result.warnings).toEqual([]); + }), ); it.effect( - "normalizes a doubled slash for a direct glob match under a trailing-slash directory component (Go afero.Glob parity)", + "normalizes a doubled slash for a direct glob match under a trailing-slash directory component", () => Effect.gen(function* () { // `splitPath` on `"schemas//*.sql"` yields `dir: "schemas/"` (the trailing slash @@ -326,30 +312,28 @@ layer(BunServices.layer)("sqlFilesGlob", (it) => { }), ); - it.effect( - "still includes a '.sql' child whose stat fails after it's already listed (Go WalkDir parity)", - () => - Effect.gen(function* () { - // Simulates the race by deleting the file the instant after `readDirectory` returns - // it, via a `FileSystem` layer that removes on the first `stat` call for that path. - const { fs, path, dir } = yield* tempWorkdir("sql-glob-stat-race-"); - const schemasDir = path.join(dir, "schemas"); - yield* fs.makeDirectory(schemasDir); - const racyFile = path.join(schemasDir, "racy.sql"); - yield* fs.writeFileString(racyFile, "select 1;"); - const racyFs: FileSystem.FileSystem = { - ...fs, - stat: (p: string) => - p === racyFile ? fs.remove(racyFile).pipe(Effect.andThen(fs.stat(p))) : fs.stat(p), - }; - const result = yield* sqlFilesGlob(racyFs, path, ["schemas"], dir); - expect(result.files).toEqual(["schemas/racy.sql"]); - expect(result.warnings).toEqual([]); - }), + it.effect("still includes a '.sql' child whose stat fails after it's already listed", () => + Effect.gen(function* () { + // Simulates the race by deleting the file the instant after `readDirectory` returns + // it, via a `FileSystem` layer that removes on the first `stat` call for that path. + const { fs, path, dir } = yield* tempWorkdir("sql-glob-stat-race-"); + const schemasDir = path.join(dir, "schemas"); + yield* fs.makeDirectory(schemasDir); + const racyFile = path.join(schemasDir, "racy.sql"); + yield* fs.writeFileString(racyFile, "select 1;"); + const racyFs: FileSystem.FileSystem = { + ...fs, + stat: (p: string) => + p === racyFile ? fs.remove(racyFile).pipe(Effect.andThen(fs.stat(p))) : fs.stat(p), + }; + const result = yield* sqlFilesGlob(racyFs, path, ["schemas"], dir); + expect(result.files).toEqual(["schemas/racy.sql"]); + expect(result.warnings).toEqual([]); + }), ); it.effect( - "fails the whole walk when a non-'.sql' child whose stat fails could have been a subdirectory (Go WalkDir parity)", + "fails the whole walk when a non-'.sql' child whose stat fails could have been a subdirectory", () => Effect.gen(function* () { const { fs, path, dir } = yield* tempWorkdir("sql-glob-dir-race-"); @@ -392,7 +376,7 @@ layer(BunServices.layer)("sqlFilesGlob", (it) => { const isRoot = typeof process.getuid === "function" && process.getuid() === 0; it.effect.skipIf(isRoot)( - "surfaces a directory-read failure during walk as a warning instead of an empty match (Go WalkDir parity)", + "surfaces a directory-read failure during walk as a warning instead of an empty match", () => Effect.gen(function* () { const { fs, path, dir } = yield* tempWorkdir("sql-glob-walk-fail-"); @@ -409,7 +393,7 @@ layer(BunServices.layer)("sqlFilesGlob", (it) => { ); it.effect.skipIf(isRoot)( - "surfaces a NESTED directory-read failure during walk with a workdir-relative path, not the matched root's (Go WalkDir parity)", + "surfaces a NESTED directory-read failure during walk with a workdir-relative path, not the matched root's", () => Effect.gen(function* () { const { fs, path, dir } = yield* tempWorkdir("sql-glob-walk-fail-nested-"); @@ -446,7 +430,7 @@ layer(BunServices.layer)("sqlFilesGlob", (it) => { ); it.effect.skipIf(isRoot)( - "picks the lexically-first failing subdirectory as the fatal error, matching Go's fs.WalkDir sorted-visit order (review CLI-1958)", + "picks the lexically-first failing subdirectory as the fatal error", () => Effect.gen(function* () { // The fake `readDirectory` returns "schemas"'s children in reverse @@ -478,21 +462,19 @@ layer(BunServices.layer)("sqlFilesGlob", (it) => { }), ); - it.effect( - "sorts direct wildcard matches by UTF-8 byte order, not UTF-16 code units (Go sort.Strings parity)", - () => - Effect.gen(function* () { - // The emoji (4-byte UTF-8, lead byte 0xF0) sorts after the fullwidth exclamation mark - // (3-byte UTF-8, lead byte 0xEF) in byte order, but before it in UTF-16 code-unit - // order (0xD83D surrogate lead vs 0xFF01) — these two characters expose the difference. - const { fs, path, dir } = yield* tempWorkdir("sql-glob-utf8-sort-"); - const schemasDir = path.join(dir, "schemas"); - yield* fs.makeDirectory(schemasDir); - yield* fs.writeFileString(path.join(schemasDir, "\u{1F600}.sql"), "select 1;"); // 😀 - yield* fs.writeFileString(path.join(schemasDir, "!.sql"), "select 2;"); // ! - const result = yield* run(["schemas/*.sql"], dir); - expect(result.files).toEqual(["schemas/!.sql", "schemas/\u{1F600}.sql"]); - expect(result.warnings).toEqual([]); - }), + it.effect("sorts direct wildcard matches by UTF-8 byte order, not UTF-16 code units", () => + Effect.gen(function* () { + // The emoji (4-byte UTF-8, lead byte 0xF0) sorts after the fullwidth exclamation mark + // (3-byte UTF-8, lead byte 0xEF) in byte order, but before it in UTF-16 code-unit + // order (0xD83D surrogate lead vs 0xFF01) — these two characters expose the difference. + const { fs, path, dir } = yield* tempWorkdir("sql-glob-utf8-sort-"); + const schemasDir = path.join(dir, "schemas"); + yield* fs.makeDirectory(schemasDir); + yield* fs.writeFileString(path.join(schemasDir, "\u{1F600}.sql"), "select 1;"); // 😀 + yield* fs.writeFileString(path.join(schemasDir, "!.sql"), "select 2;"); // ! + const result = yield* run(["schemas/*.sql"], dir); + expect(result.files).toEqual(["schemas/!.sql", "schemas/\u{1F600}.sql"]); + expect(result.warnings).toEqual([]); + }), ); }); diff --git a/apps/cli/src/command-internal/sql-split.ts b/apps/cli/src/command-internal/sql-split.ts index 66c7a71742..361f8f74ab 100644 --- a/apps/cli/src/command-internal/sql-split.ts +++ b/apps/cli/src/command-internal/sql-split.ts @@ -9,7 +9,11 @@ interface State { /** Returns the next state, or `null` to emit a token (statement boundary). */ - next(rune: string, data: string): State | null; + next(char: string, data: string): State | null; + /** `true` while inside a block comment, quoted string, quoted identifier or dollar quote. */ + readonly open?: boolean; + /** `true` while inside a `BEGIN ATOMIC` body, where `;` no longer ends a statement. */ + readonly atomic?: boolean; } const BEGIN_ATOMIC = "ATOMIC"; @@ -17,17 +21,17 @@ const END_ATOMIC = "END"; // PostgreSQL's scan.l treats every code point at or above 0x80 as an identifier/dollar-tag // character (`ident_cont`/`dolq_cont`), whatever its Unicode category. -const isIdentifierRune = (rune: string): boolean => { - const codePoint = rune.codePointAt(0); - return codePoint !== undefined && (codePoint >= 0x80 || /[A-Za-z0-9_$]/u.test(rune)); +const isIdentifierChar = (char: string): boolean => { + const codePoint = char.codePointAt(0); + return codePoint !== undefined && (codePoint >= 0x80 || /[A-Za-z0-9_$]/u.test(char)); }; // A code point spans at most two UTF-16 units, so the last one before `offset` lies within // the preceding two. -const hasIdentifierRuneBefore = (data: string, offset: number): boolean => { +const hasIdentifierCharBefore = (data: string, offset: number): boolean => { if (offset <= 0) return false; - const rune = Array.from(data.slice(Math.max(0, offset - 2), offset)).at(-1); - return rune !== undefined && isIdentifierRune(rune); + const char = Array.from(data.slice(Math.max(0, offset - 2), offset)).at(-1); + return char !== undefined && isIdentifierChar(char); }; const asciiUpper = (text: string): string => text.replace(/[a-z]/g, (c) => c.toUpperCase()); @@ -35,13 +39,13 @@ const asciiUpper = (text: string): string => text.replace(/[a-z]/g, (c) => c.toU function endsWithKeyword(data: string, keyword: string): boolean { const offset = data.length - keyword.length; if (offset < 0 || asciiUpper(data.slice(offset)) !== keyword) return false; - return !hasIdentifierRuneBefore(data, offset); + return !hasIdentifierCharBefore(data, offset); } -const isSqlWhitespace = (rune: string): boolean => " \t\n\r\f\v".includes(rune); +const isSqlWhitespace = (char: string): boolean => " \t\n\r\f\v".includes(char); // scan.l `newline`: a `--` comment ends at either. -const isNewline = (rune: string): boolean => rune === "\n" || rune === "\r"; +const isNewline = (char: string): boolean => char === "\n" || char === "\r"; function isBeginAtomic(data: string): boolean { if (!endsWithKeyword(data, BEGIN_ATOMIC)) return false; @@ -50,50 +54,72 @@ function isBeginAtomic(data: string): boolean { return endsWithKeyword(data.slice(0, end), "BEGIN"); } -function isCommentsAndWhitespace(text: string): boolean { +// A character that ends a delimiter can't start the next one: `/*/` opens a comment without +// closing it, and `*/*` closes one before a lone `*`. +const blockDelimiterDelta = (window: string): -1 | 0 | 1 => + window === "/*" ? 1 : window === "*/" ? -1 : 0; + +// Index of the first character past leading blanks and comments, or `text.length` when only +// trivia (or an unterminated comment) remains. +function skipLeadingTrivia(text: string, isBlank: (char: string) => boolean): number { let i = 0; while (i < text.length) { - if (isSqlWhitespace(text[i]!)) { + if (isBlank(text[i]!)) { i += 1; } else if (text.startsWith("--", i)) { const newline = text.slice(i + 2).search(/[\n\r]/u); - if (newline === -1) return true; + if (newline === -1) return text.length; i += newline + 3; } else if (text.startsWith("/*", i)) { - // Match `BlockState`'s sliding-window scan so both agree on overlapping delimiters. let depth = 1; + let afterDelimiter = true; i += 2; while (i < text.length && depth > 0) { - const window = text.slice(i - 1, i + 1); - if (window === "/*") depth += 1; - else if (window === "*/") depth -= 1; + const delta: number = afterDelimiter ? 0 : blockDelimiterDelta(text.slice(i - 1, i + 1)); + afterDelimiter = delta !== 0; + depth += delta; i += 1; } - if (depth > 0) return true; + if (depth > 0) return text.length; } else { - return false; + return i; } } - return true; + return text.length; } +const isCommentsAndWhitespace = (text: string): boolean => + skipLeadingTrivia(text, isSqlWhitespace) === text.length; + +const BYTE_ORDER_MARK = "\uFEFF"; + +/** + * Strips leading whitespace, byte order marks and line/block comments (block comments nest, as + * PostgreSQL scans them) so a keyword check sees the statement's first token. Empty when only + * trivia or an unterminated comment remains. + */ +export const trimLeadingSqlComments = (sql: string): string => + sql + .slice(skipLeadingTrivia(sql, (char) => isSqlWhitespace(char) || char === BYTE_ORDER_MARK)) + .trim(); + class ReadyState implements State { - next(rune: string, data: string): State | null { - switch (rune) { + next(char: string, data: string): State | null { + switch (char) { case "$": { - // A `$` after an identifier rune continues the identifier (`pending$$foo$`), not a + // A `$` after an identifier char continues the identifier (`pending$$foo$`), not a // dollar quote. A digit counts too (`1$$`), unlike PostgreSQL; valid SQL never has that. - const offset = data.length - rune.length; - if (hasIdentifierRuneBefore(data, offset)) return this; + const offset = data.length - char.length; + if (hasIdentifierCharBefore(data, offset)) return this; return new TagState(offset); } case "'": // `E'…'` is an escape string constant only when the `E` starts a token (scan.l // `xestart`); in `type'…'` it ends an identifier. A digit or `$` before the `E` // counts as one too, unlike PostgreSQL; valid SQL never has that. - return new QuoteState(rune, endsWithKeyword(data.slice(0, -1), "E")); + return new QuoteState(char, endsWithKeyword(data.slice(0, -1), "E")); case '"': - return new QuoteState(rune, false); + return new QuoteState(char, false); case "-": return new CommentState(); case "/": @@ -115,32 +141,30 @@ class ReadyState implements State { } class CommentState implements State { - next(rune: string, data: string): State | null { - if (rune === "-") return new LineCommentState(); - return new ReadyState().next(rune, data); + next(char: string, data: string): State | null { + if (char === "-") return new LineCommentState(); + return new ReadyState().next(char, data); } } class LineCommentState implements State { - next(rune: string): State { - return isNewline(rune) ? new ReadyState() : this; + next(char: string): State { + return isNewline(char) ? new ReadyState() : this; } } class BlockState implements State { private depth = 0; - next(rune: string, data: string): State | null { - const window = data.slice(-2); - if (window === "/*") { - this.depth += 1; - return this; - } - if (this.depth === 0) return new ReadyState().next(rune, data); - if (window === "*/") { - this.depth -= 1; - if (this.depth === 0) return new ReadyState(); - } - return this; + private afterDelimiter = false; + get open(): boolean { + return this.depth > 0; + } + next(char: string, data: string): State | null { + const delta = this.afterDelimiter ? 0 : blockDelimiterDelta(data.slice(-2)); + this.afterDelimiter = delta !== 0; + if (this.depth === 0 && delta === 0) return new ReadyState().next(char, data); + this.depth += delta; + return this.depth === 0 ? new ReadyState() : this; } } @@ -151,26 +175,29 @@ class QuoteState implements State { private readonly delimiter: string, private readonly backslashEscapes: boolean, ) {} - next(rune: string, data: string): State | null { + get open(): boolean { + return !this.escape; + } + next(char: string, data: string): State | null { if (this.escape) { // Preserve a doubled quote ('' or ""). - if (rune === this.delimiter) { + if (char === this.delimiter) { this.escape = false; return this; } - if (this.backslashEscapes) return new QuoteContinueState().next(rune, data); - return new ReadyState().next(rune, data); + if (this.backslashEscapes) return new QuoteContinueState().next(char, data); + return new ReadyState().next(char, data); } if (this.backslash) { - // Preserve the rune after a backslash (\' or \\). + // Preserve the char after a backslash (\' or \\). this.backslash = false; return this; } - if (this.backslashEscapes && rune === "\\") { + if (this.backslashEscapes && char === "\\") { this.backslash = true; return this; } - if (rune === this.delimiter) this.escape = true; + if (char === this.delimiter) this.escape = true; return this; } } @@ -180,32 +207,33 @@ class QuoteState implements State { class QuoteContinueState implements State { private newline = false; private dashes = 0; - next(rune: string, data: string): State | null { + next(char: string, data: string): State | null { if (this.dashes === 2) { - if (isNewline(rune)) { + if (isNewline(char)) { this.dashes = 0; this.newline = true; } return this; } - if (rune === "-") { + if (char === "-") { this.dashes += 1; return this; } if (this.dashes === 0) { - if (isSqlWhitespace(rune)) { - this.newline ||= isNewline(rune); + if (isSqlWhitespace(char)) { + this.newline ||= isNewline(char); return this; } - if (this.newline && rune === "'") return new QuoteState(rune, true); + if (this.newline && char === "'") return new QuoteState(char, true); } - return new ReadyState().next(rune, data); + return new ReadyState().next(char, data); } } class DollarState implements State { + readonly open = true; constructor(private readonly delimiter: string) {} - next(_rune: string, data: string): State | null { + next(_char: string, data: string): State | null { if (data.slice(-this.delimiter.length) === this.delimiter) return new ReadyState(); return this; } @@ -213,10 +241,10 @@ class DollarState implements State { class TagState implements State { constructor(private readonly offset: number) {} - next(rune: string, data: string): State | null { - if (rune === "$") return new DollarState(data.slice(this.offset)); - if (isIdentifierRune(rune)) return this; - return new ReadyState().next(rune, data); + next(char: string, data: string): State | null { + if (char === "$") return new DollarState(data.slice(this.offset)); + if (isIdentifierChar(char)) return this; + return new ReadyState().next(char, data); } } @@ -228,19 +256,26 @@ class EscapeState implements State { class ParenState implements State { constructor(private prev: State) {} - next(rune: string, data: string): State | null { - const curr = this.prev.next(rune, data); + get open(): boolean { + return this.prev.open === true; + } + get atomic(): boolean { + return this.prev.atomic === true; + } + next(char: string, data: string): State | null { + const curr = this.prev.next(char, data); if (curr === null) { this.prev = new ReadyState(); return this; } this.prev = curr; if (!(this.prev instanceof ReadyState)) return this; - return rune === ")" ? new ReadyState() : this; + return char === ")" ? new ReadyState() : this; } } class AtomicState implements State { + readonly atomic = true; private pendingEnd = false; private statementStart: number; private statementHasContent = false; @@ -250,12 +285,15 @@ class AtomicState implements State { ) { this.statementStart = start; } - next(rune: string, data: string): State | null { + get open(): boolean { + return this.prev.open === true; + } + next(char: string, data: string): State | null { const pendingEnd = this.pendingEnd; this.pendingEnd = false; - if (pendingEnd && !isIdentifierRune(rune)) return new ReadyState().next(rune, data); + if (pendingEnd && !isIdentifierChar(char)) return new ReadyState().next(char, data); // An `END` inside a nested quote/comment doesn't count. - const curr = this.prev.next(rune, data); + const curr = this.prev.next(char, data); if (curr === null) { this.prev = new ReadyState(); this.statementStart = data.length; @@ -289,30 +327,40 @@ interface RawToken { readonly terminated: boolean; } +interface RawSplit { + readonly tokens: RawToken[]; + /** The input ends inside a block comment, quoted string, quoted identifier or dollar quote. */ + readonly unterminated: boolean; + /** The input holds a `BEGIN ATOMIC` body, which the splitter reads as part of one statement. */ + readonly beginAtomic: boolean; +} + /** The FSM traversal shared by every `splitSql*` entry point below. */ -function splitRaw(sql: string): RawToken[] { +function splitRaw(sql: string): RawSplit { let state: State = new ReadyState(); + let beginAtomic = false; const tokens: RawToken[] = []; // Slice each token from `sql` instead of growing it with `+=`: states read the token's tail every - // rune, which would rebuild the whole string each time and go quadratic on large tokens. `data` + // char, which would rebuild the whole string each time and go quadratic on large tokens. `data` // starts at the token, so offsets held by states are token-relative. let start = 0; let end = 0; - for (const rune of sql) { - end += rune.length; + for (const char of sql) { + end += char.length; const data = sql.slice(start, end); - const next = state.next(rune, data); + const next = state.next(char, data); if (next === null) { tokens.push({ text: data, terminated: true }); start = end; state = new ReadyState(); } else { state = next; + beginAtomic ||= state.atomic === true; } } // Trailing non-terminated statement at EOF. if (end > start) tokens.push({ text: sql.slice(start), terminated: false }); - return tokens; + return { tokens, unterminated: state.open === true, beginAtomic }; } /** @@ -324,7 +372,7 @@ export function splitSql( ...transform: ReadonlyArray<(s: string) => string> ): string[] { const statements: string[] = []; - for (const { text: raw } of splitRaw(sql)) { + for (const { text: raw } of splitRaw(sql).tokens) { let token = raw; for (const apply of transform) token = apply(token); if (token.length > 0) statements.push(token); @@ -340,6 +388,20 @@ export function splitAndTrim(sql: string): string[] { return splitSql(sql, trimStatement); } +/** + * `splitAndTrim`, plus whether `sql` ends inside a block comment, quote or dollar quote, and + * whether the splitter read a `BEGIN ATOMIC` body, whose inner statements `statements` omits. + */ +export function splitAndTrimChecked(sql: string): { + readonly statements: string[]; + readonly unterminated: boolean; + readonly beginAtomic: boolean; +} { + const { tokens, unterminated, beginAtomic } = splitRaw(sql); + const statements = tokens.map(({ text }) => trimStatement(text)).filter((s) => s.length > 0); + return { statements, unterminated, beginAtomic }; +} + /** One statement, paired with both its raw and trimmed forms. */ export interface SplitSqlToken { /** The exact text `splitSql(sql)` (no transforms) would emit for this statement. */ @@ -364,7 +426,7 @@ export interface SplitSqlToken { * empty, so callers counting only non-empty trimmed statements still see every raw token. */ export function splitSqlTokens(sql: string): ReadonlyArray { - return splitRaw(sql).map(({ text: raw, terminated }) => ({ + return splitRaw(sql).tokens.map(({ text: raw, terminated }) => ({ raw, trimmed: trimStatement(raw), terminated, diff --git a/apps/cli/src/command-internal/sql-split.unit.test.ts b/apps/cli/src/command-internal/sql-split.unit.test.ts index b904d19e85..913748fec6 100644 --- a/apps/cli/src/command-internal/sql-split.unit.test.ts +++ b/apps/cli/src/command-internal/sql-split.unit.test.ts @@ -1,6 +1,13 @@ import { describe, expect, it } from "vitest"; -import { findDropStatements, splitAndTrim, splitSql, splitSqlTokens } from "./sql-split.ts"; +import { + findDropStatements, + splitAndTrim, + splitAndTrimChecked, + splitSql, + splitSqlTokens, + trimLeadingSqlComments, +} from "./sql-split.ts"; describe("splitAndTrim", () => { it("splits simple statements and trims trailing ; + whitespace", () => { @@ -142,6 +149,35 @@ describe("splitAndTrim", () => { ]); }); + it.each([ + ["a closing */ directly followed by *", "/* /* */* x */ COMMIT; DELETE FROM t; SELECT 1"], + ["a closing */ directly followed by /*", "/* /* *//* */ */ COMMIT; DELETE FROM t; SELECT 1"], + ["a doubled star before the closing slash", "/* /* **/ */ COMMIT; DELETE FROM t; SELECT 1"], + ])("closes a nested block comment with %s", (_name, sql) => { + const statements = splitAndTrim(sql); + expect(statements).toHaveLength(3); + expect(statements.slice(1)).toEqual(["DELETE FROM t", "SELECT 1"]); + expect(statements[0]!.endsWith("COMMIT")).toBe(true); + }); + + it("does not let /*/ close the comment it opens", () => { + expect(splitAndTrim("SELECT 1 /*/ ; */ ; SELECT 2")).toEqual(["SELECT 1 /*/ ; */", "SELECT 2"]); + }); + + it("reads /*/ inside a comment as a nested opener only", () => { + expect(splitAndTrim("/* a /*/ b */ ; */ SELECT 1; SELECT 2")).toEqual([ + "/* a /*/ b */ ; */ SELECT 1", + "SELECT 2", + ]); + }); + + it("closes a comment written as /**/ and /***/", () => { + expect(splitAndTrim("SELECT /**/ 1; SELECT /***/ 2")).toEqual([ + "SELECT /**/ 1", + "SELECT /***/ 2", + ]); + }); + it("does not split inside a BEGIN ATOMIC body", () => { const sql = "CREATE FUNCTION f() RETURNS int LANGUAGE sql BEGIN ATOMIC SELECT 1; SELECT 2; END; SELECT 3;"; @@ -234,7 +270,7 @@ describe("splitAndTrim", () => { it.each(["\u00A0", "\uFEFF", "\u0085"])( "does not treat BEGIN %s ATOMIC as the keyword pair", (gap) => { - // Only PostgreSQL's own whitespace separates the keywords; these are identifier runes. + // Only PostgreSQL's own whitespace separates the keywords; these are identifier characters. const sql = `BEGIN ${gap} ATOMIC; SELECT 1; end; SELECT 2;`; expect(splitAndTrim(sql)).toEqual([`BEGIN ${gap} ATOMIC`, "SELECT 1", "end", "SELECT 2"]); }, @@ -247,9 +283,9 @@ describe("splitAndTrim", () => { ]); }); - it("does not close a BEGIN ATOMIC body at an END after an overlapping block comment", () => { + it("does not close a BEGIN ATOMIC body at an END after a block comment nesting /*/", () => { const body = - "CREATE FUNCTION f() RETURNS int LANGUAGE sql BEGIN ATOMIC SELECT 1; /* a /*/ b */ SELECT 2 END; SELECT 3; END"; + "CREATE FUNCTION f() RETURNS int LANGUAGE sql BEGIN ATOMIC SELECT 1; /* a /*/ b */ */ SELECT 2 END; SELECT 3; END"; expect(splitAndTrim(`${body}; SELECT 4;`)).toEqual([body, "SELECT 4"]); }); @@ -262,6 +298,68 @@ describe("splitAndTrim", () => { }); }); +describe("splitAndTrimChecked", () => { + it("returns the statements splitAndTrim returns", () => { + const sql = "SELECT 1; /* a */ SELECT 'b;';\nSELECT $$c; d$$"; + expect(splitAndTrimChecked(sql).statements).toEqual(splitAndTrim(sql)); + }); + + it.each([ + ["an open block comment", "SELECT 1 /* a"], + ["an open nested block comment", "SELECT 1 /* a /* b */"], + ["an open string", "SELECT 'a"], + ["a string ending in a doubled quote", "SELECT 'a''"], + ["an open quoted identifier", 'SELECT "a'], + ["an open escape string", String.raw`SELECT E'a\'`], + ["an open dollar quote", "SELECT $$a"], + ["an open tagged dollar quote", "SELECT $t$a$$"], + ["an open string inside parentheses", "SELECT ('a"], + ["an open comment after a statement", "SELECT 1; /*"], + ])("flags %s as unterminated", (_name, sql) => { + expect(splitAndTrimChecked(sql).unterminated).toBe(true); + }); + + it.each([ + ["a plain statement", "SELECT 1"], + ["a trailing line comment", "SELECT 1 -- a"], + ["a closed block comment", "SELECT 1 /* a */"], + ["a closed nested block comment", "SELECT 1 /* a /* b */ */"], + ["a closed string", "SELECT 'a'"], + ["a string with a doubled quote", "SELECT 'a''b'"], + ["a closed quoted identifier", 'SELECT "a"'], + ["a closed escape string", String.raw`SELECT E'a\''`], + ["a closed dollar quote", "SELECT $$a;b$$"], + ["a closed string inside parentheses", "SELECT ('a')"], + ["blank input", " \n"], + ])("does not flag %s", (_name, sql) => { + expect(splitAndTrimChecked(sql).unterminated).toBe(false); + }); + + it.each([ + ["a function body", "CREATE FUNCTION f() BEGIN ATOMIC SELECT 1; END"], + ["a lowercase pair split by a newline", "SELECT 1 FROM begin\n atomic; COMMIT; END"], + ["a pair inside parentheses", "SELECT (SELECT 1 FROM begin atomic; COMMIT; END)"], + ["a pair next to a string holding the same words", "SELECT 'begin atomic' FROM begin atomic"], + ])("reports %s as a BEGIN ATOMIC body", (_name, sql) => { + expect(splitAndTrimChecked(sql).beginAtomic).toBe(true); + }); + + it.each([ + ["a string", "SELECT 'begin atomic'"], + ["a string with a newline between the words", "SELECT 'begin\natomic'"], + ["an escape string", String.raw`SELECT E'it\'s begin atomic'`], + ["a Unicode escape string", "SELECT U&'begin atomic'"], + ["a dollar quote", "SELECT $$begin atomic$$"], + ["a quoted identifier", 'SELECT 1 AS "begin atomic"'], + ["a line comment", "SELECT 1 -- begin atomic\n; SELECT 2"], + ["a block comment", "SELECT 1 /* begin atomic */"], + ["an identifier that merely ends in begin", "SELECT 1 xbegin atomic; SELECT 2"], + ["a plain statement", "SELECT 1"], + ])("does not report %s as a BEGIN ATOMIC body", (_name, sql) => { + expect(splitAndTrimChecked(sql).beginAtomic).toBe(false); + }); +}); + describe("splitSql", () => { it("preserves raw statements (no transforms) including the trailing ;-less token", () => { expect(splitSql("SELECT 1; SELECT 2")).toEqual(["SELECT 1;", " SELECT 2"]); @@ -280,3 +378,34 @@ describe("findDropStatements", () => { expect(findDropStatements(sql)).toEqual(["DROP TABLE real"]); }); }); + +describe("trimLeadingSqlComments", () => { + it.each([ + ["whitespace", " \t\n\r\f\vSELECT 1"], + ["a byte order mark", "\uFEFF SELECT 1"], + ["a line comment", "-- a\nSELECT 1"], + ["a carriage-return line comment", "-- a\rSELECT 1"], + ["a block comment", "/* a */SELECT 1"], + ["a nested block comment", "/* a /* b */ c */ SELECT 1"], + ["a nested comment closed by */ then *", "/* /* */* x */ SELECT 1"], + ["a nested comment closed by */ then /*", "/* /* *//* */ */ SELECT 1"], + ["a comment nesting /*/", "/* a /*/ b */ */ SELECT 1"], + ["stacked comments and blanks", "\uFEFF-- a\n /* b */ \n-- c\nSELECT 1"], + ])("strips %s", (_name, sql) => { + expect(trimLeadingSqlComments(sql)).toBe("SELECT 1"); + }); + + it("keeps comments that are not at the start", () => { + expect(trimLeadingSqlComments("SELECT 1 -- a\n")).toBe("SELECT 1 -- a"); + }); + + it.each([ + ["a line comment without a newline", "-- a"], + ["an unterminated block comment", "/* a SELECT 1"], + ["a comment opened by /*/ and closed later", "/*/ SELECT 1 */"], + ["a nested comment closed only once", "/* /* */* x SELECT 1"], + ["only whitespace", " \n"], + ])("returns empty for %s", (_name, sql) => { + expect(trimLeadingSqlComments(sql)).toBe(""); + }); +}); diff --git a/apps/cli/src/command-internal/stack-auth-config.ts b/apps/cli/src/command-internal/stack-auth-config.ts index d5f818e5d9..c6529a6b70 100644 --- a/apps/cli/src/command-internal/stack-auth-config.ts +++ b/apps/cli/src/command-internal/stack-auth-config.ts @@ -28,6 +28,7 @@ const passThrough = section; interface ResolvedAuthOptions { + readonly smtpEnabled: boolean; readonly authExternalUrl?: string; readonly apiExternalUrl?: string; readonly passkeyEnabled?: boolean; @@ -115,7 +116,7 @@ export const resolveAuthConfig = Effect.fn("StackAuthConfig.resolve")( web3: passThrough()(auth.web3), oauthServer: passThrough()(auth.oauth_server), }, - ...(auth.email.smtp?.enabled !== true + ...(options.smtpEnabled !== true || auth.email.smtp === undefined ? localSmtp.enabled ? { smtpAdminEmail: localSmtp.admin_email ?? "admin@email.com", diff --git a/apps/cli/src/command-internal/stack-backend.ts b/apps/cli/src/command-internal/stack-backend.ts index bbe6354c8c..850836704b 100644 --- a/apps/cli/src/command-internal/stack-backend.ts +++ b/apps/cli/src/command-internal/stack-backend.ts @@ -9,12 +9,12 @@ import { type Path, } from "effect"; import { extractCommandPath, hasRootVersionFlag } from "../shared/cli/run.ts"; -import type { ExperimentalFeatureFlagError } from "./experimental-feature.ts"; +import type { ExperimentalFeatureFlagError } from "../shared/config/experimental-feature.ts"; import { projectStateMode, type ProjectStateMode, softProjectStateMode, -} from "./project-state-mode.ts"; +} from "../shared/config/project-state-mode.ts"; import { actionability, type CliErrorActionabilityDeclaration, @@ -89,7 +89,6 @@ export const resolveProjectStateSelection = Effect.fn("ProjectState.resolveSelec function* (input: { readonly args: ReadonlyArray; readonly cwd: string; - readonly env: Readonly>; }): Effect.fn.Return< { readonly projectState: Exit.Exit | undefined; diff --git a/apps/cli/src/command-internal/stack-config.precedence.integration.test.ts b/apps/cli/src/command-internal/stack-config.precedence.integration.test.ts new file mode 100644 index 0000000000..ee5b8bbd84 --- /dev/null +++ b/apps/cli/src/command-internal/stack-config.precedence.integration.test.ts @@ -0,0 +1,195 @@ +import { BunServices } from "@effect/platform-bun"; +import { describe, it } from "@effect/vitest"; +import { ConfigProvider, Effect, FileSystem, Layer, Path } from "effect"; + +import { mockCommandSettings, useTempWorkdir } from "../../tests/helpers/command-mocks.ts"; +import { cliConfigValuesTestLayer } from "../../tests/helpers/config-values-layer.ts"; +import { ConfigEnvPins } from "../../tests/helpers/config-env-pins.ts"; +import { goldenJson, useShellEnvPin } from "../../tests/helpers/config-goldens.ts"; +import { runtimeInfoLayer } from "../shared/runtime/runtime-info.layer.ts"; +import { loadStackConfig } from "./stack-config.ts"; + +const TARGET_REF = "abcdefghijklmnopqrst"; +const STACK_ID = "golden-stack"; +const GOLDEN_DIR = "./testdata/config-precedence/stack-config"; + +const BASE_CONFIG = `project_id = "golden-stack" + +[api] +port = 55321 +schemas = ["public", "base_api"] + +[db] +port = 55322 + +[auth] +site_url = "http://base.example.com" +enable_signup = true + +[studio] +port = 55323 + +[realtime] +enabled = true + +[storage] +enabled = true +`; + +const REMOTE_BLOCK = ` +[remotes.prod] +project_id = "${TARGET_REF}" + +[remotes.prod.api] +schemas = ["public", "remote_api"] + +[remotes.prod.auth] +site_url = "http://remote.example.com" +enable_signup = false + +[remotes.prod.realtime] +enabled = false +`; + +const SHELL_ENV = { + SUPABASE_API_PORT: "56321", + SUPABASE_DB_PORT: "56322", + SUPABASE_API_SCHEMAS: "public,shell_api", + SUPABASE_AUTH_SITE_URL: "http://shell.example.com", + SUPABASE_AUTH_ENABLE_SIGNUP: "false", + SUPABASE_REALTIME_ENABLED: "false", + SUPABASE_STUDIO_ENABLED: "false", +}; + +const DOTENV = [ + "SUPABASE_API_PORT=57321", + "SUPABASE_DB_PORT=57322", + "SUPABASE_API_SCHEMAS=public,dotenv_api", + "SUPABASE_AUTH_SITE_URL=http://dotenv.example.com", + "SUPABASE_AUTH_ENABLE_SIGNUP=false", + "SUPABASE_REALTIME_ENABLED=false", + "SUPABASE_STUDIO_ENABLED=false", + "", +].join("\n"); + +interface Fixture { + readonly golden: string; + readonly name: string; + readonly config: string; + readonly shellEnv?: Readonly>; + readonly dotenv?: string; + readonly contextRef?: string; +} + +const FIXTURES: ReadonlyArray = [ + { + golden: "no-overrides", + name: "stack config with base config and no env resolves the base values (pre-refactor)", + config: BASE_CONFIG, + }, + { + golden: "remote-block-ignored", + name: "stack config ignores a [remotes.*] block because no caller passes a ref (pre-refactor)", + config: BASE_CONFIG + REMOTE_BLOCK, + shellEnv: { SUPABASE_REMOTES_PROD_PROJECT_ID: TARGET_REF }, + }, + { + golden: "remote-block-merged-via-context", + name: "stack config applies a remote block only when the caller preloads a ref-scoped context (pre-refactor)", + config: BASE_CONFIG + REMOTE_BLOCK, + contextRef: TARGET_REF, + }, + { + golden: "remote-block-merged-via-context-with-shell-env", + name: "stack config lets shell env beat a merged remote block for the same keys (pre-refactor)", + config: BASE_CONFIG + REMOTE_BLOCK, + shellEnv: SHELL_ENV, + contextRef: TARGET_REF, + }, + { + golden: "shell-env", + name: "stack config shell env overrides ports, api schemas, auth settings and service gates (pre-refactor)", + config: BASE_CONFIG, + shellEnv: SHELL_ENV, + }, + { + golden: "project-dotenv", + name: "stack config supabase/.env overrides ports, api schemas, auth settings and service gates (pre-refactor)", + config: BASE_CONFIG, + dotenv: DOTENV, + }, + { + golden: "shell-beats-dotenv", + name: "stack config shell env beats supabase/.env for the same keys (pre-refactor)", + config: BASE_CONFIG, + shellEnv: { + SUPABASE_API_SCHEMAS: "public,shell_api", + SUPABASE_AUTH_SITE_URL: "http://shell.example.com", + }, + dotenv: DOTENV, + }, +]; + +const configEnvOnly = (env: Readonly>) => + Object.fromEntries(Object.entries(env).filter(([name]) => name.startsWith("SUPABASE_"))); + +const tempRoot = useTempWorkdir("stack-config-golden-"); + +describe("stack config precedence goldens", () => { + const pinShellEnv = useShellEnvPin(); + + for (const fixture of FIXTURES) { + it.live(fixture.name, (ctx) => + Effect.gen(function* () { + const workdir = tempRoot.current; + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs.makeDirectory(path.join(workdir, "supabase"), { recursive: true }); + yield* fs.writeFileString(path.join(workdir, "supabase", "config.toml"), fixture.config); + if (fixture.dotenv !== undefined) { + yield* fs.writeFileString(path.join(workdir, "supabase", ".env"), fixture.dotenv); + } + pinShellEnv(fixture.shellEnv ?? {}); + + const layer = Layer.mergeAll( + BunServices.layer, + runtimeInfoLayer, + mockCommandSettings({ workdir, supabaseHome: workdir }), + cliConfigValuesTestLayer, + Layer.succeed(ConfigEnvPins, fixture.shellEnv ?? {}), + Layer.succeed( + ConfigProvider.ConfigProvider, + ConfigProvider.fromEnvRecord( + Object.fromEntries( + Object.entries(process.env).flatMap(([name, value]) => + value === undefined ? [] : [[name, value]], + ), + ), + { preserveEmptyStrings: true }, + ), + ), + ); + + const config = yield* loadStackConfig( + workdir, + fixture.contextRef === undefined ? undefined : { projectRef: fixture.contextRef }, + ).pipe(Effect.provide(layer)); + + const creations = yield* config.creations(STACK_ID); + const keys = yield* config.keys; + const golden = { + // Pins come from the artifact catalog, not config precedence. + creations: creations.map(({ pin: _pin, ...creation }) => creation), + keys, + source: config.source, + projectEnvValues: configEnvOnly(config.projectEnvValues), + }; + yield* Effect.promise(() => + ctx + .expect(goldenJson(golden, { [workdir]: "" })) + .toMatchFileSnapshot(`${GOLDEN_DIR}/${fixture.golden}.json`), + ); + }).pipe(Effect.provide(BunServices.layer)), + ); + } +}); diff --git a/apps/cli/src/command-internal/stack-config.ts b/apps/cli/src/command-internal/stack-config.ts index 5e897e7d05..2f89f78f36 100644 --- a/apps/cli/src/command-internal/stack-config.ts +++ b/apps/cli/src/command-internal/stack-config.ts @@ -1,7 +1,5 @@ import { CommandSettings } from "../config/command-settings.service.ts"; import { getDefaultCliConfig, type CliConfig } from "@supabase/config"; -import { resolveCliConfigSubtree } from "@supabase/config/internal"; -import { validateCliConfig } from "@supabase/config/effect"; import { DEFAULT_LOCAL_S3_ACCESS_KEY_ID, DEFAULT_LOCAL_S3_REGION, @@ -10,14 +8,26 @@ import { } from "@supabase/stack/defaults"; import { type ServiceCreationInput as ServiceCreationType } from "@supabase/stack/effect"; import { type ArtifactSelection } from "@supabase/stack/internal/artifacts"; -import { Crypto, Effect, Data, FileSystem, Path, Redacted, Schema, SchemaIssue } from "effect"; +import { Crypto, Effect, Data, FileSystem, Option, Path, Redacted, Schema } from "effect"; import { FetchHttpClient } from "effect/http"; -import { loadLocalProjectContext, type LocalProjectContext } from "./local-project-context.ts"; +import { CliConfigKeys, type AnyCliConfigKey } from "../config/cli-config-keys.ts"; +import { resolveConfigSubtree } from "../config/cli-config-subtree.ts"; +import type { + CliConfigMaterialized, + CliConfigValues, +} from "../config/cli-config-values.service.ts"; import { RuntimeInfo } from "../shared/runtime/runtime-info.service.ts"; import { CLI_VERSION } from "../shared/cli/version.ts"; +import { decryptSecretMap } from "../shared/config/vault-decrypt.ts"; +import { + describeConfigLoadFailure, + loadResolvedConfigContext, + resolvePasskeyWebauthn, +} from "./resolved-config-context.ts"; import { resolveAuthConfig } from "./stack-auth-config.ts"; -import { parseGoDuration } from "./go-duration.ts"; +import { resolveSmtpEnabled } from "./smtp-enabled.ts"; +import { parseDuration } from "./duration.ts"; import { parseFileSizeLimit } from "./storage-bucket-config.ts"; import { stackArtifactSelection } from "./stack-artifact-selection.ts"; import { @@ -27,42 +37,11 @@ import { } from "./stack-lock.ts"; import { - decryptAuthSecret, resolveJwtSecret, resolveConfiguredSigningKeys, - envOverride, - envOverrideApiMaxRows, - envOverrideAuthPasswordRequirements, - envOverrideAnalyticsBackend, - envOverrideBool, - envOverrideDefaultPoolSize, - envOverrideDenoVersion, - envOverrideEdgeRuntimePolicy, - envOverrideMaxClientConn, - envOverrideMajorVersion, - envOverrideOrioleDBVersion, - envOverridePort, - envOverridePoolMode, - envOverrideRealtimeIpVersion, - envOverrideRealtimeMaxHeaderLength, - envOverrideUint, - resolveAuthCaptcha, - resolveAuthEmail, - resolveAuthEmailSmtp, - resolveAuthExternalUrl, resolveAuthExternalProviders, - resolveAuthHooks, - resolveAuthMfa, - resolveAuthSms, - resolveDbSettingsEnvOverrides, - resolveGotrueOAuthServer, - resolveGotruePasskeyWebauthn, - resolveGotrueRateLimit, - resolveGotrueSessions, - resolveGotrueWeb3, - strToArr, } from "./local-config-values.ts"; -import { generateAsymmetricGoJwt } from "./go-jwt.ts"; +import { generateAsymmetricLocalJwt } from "./local-jwt.ts"; import { recordOrioleDbTelemetry } from "./db-image.ts"; import { resolveRemoteJwks, @@ -93,7 +72,7 @@ interface StackStartConfig { ) => Effect.Effect, StackConfigError>; readonly source: CliConfig; readonly projectEnvValues: Readonly>; - readonly document?: Record; + readonly originAt: CliConfigMaterialized["originAt"]; readonly remoteJwks: Effect.Effect; readonly keys: Effect.Effect< { @@ -114,7 +93,13 @@ interface StackStartConfig { type StackConfigEffect = Effect.Effect< StackStartConfig, StackConfigError | LockResolutionError | E, - FileSystem.FileSystem | Path.Path | RuntimeInfo | Crypto.Crypto | CommandSettings | R + | FileSystem.FileSystem + | Path.Path + | RuntimeInfo + | Crypto.Crypto + | CommandSettings + | CliConfigValues + | R >; /** Host directory holding a stack's Storage objects, beside its runtime state. */ @@ -125,8 +110,8 @@ export const stackUploadsDir = ( ): string => path.join(supabaseHome, "stacks", stackId, "uploads"); interface StackConfigOptions { - /** An already-loaded project context to reuse. */ - readonly context?: LocalProjectContext; + /** The hosted project whose `[remotes.*]` block applies. */ + readonly projectRef?: string; /** Chooses the artifact pins instead of the bundled ones; without a `supabase/config.toml` the bundled pins apply, or a frozen run fails. */ readonly selection?: ( request: LockRequest, @@ -146,729 +131,39 @@ const isRecord = (value: unknown): value is Readonly> => const encodeJwkArray = Schema.encodeSync(Schema.fromJsonString(Schema.Array(Schema.Unknown))); -const withoutUndefined = (value: unknown): unknown => { - if (Redacted.isRedacted(value)) return value; - if (Array.isArray(value)) return value.map(withoutUndefined); - if (!isRecord(value)) return value; - return Object.fromEntries( - Object.entries(value) - .filter(([, item]) => item !== undefined) - .map(([key, item]) => [key, withoutUndefined(item)]), - ); -}; - const section = (document: Readonly> | undefined, name: string) => { const value = document?.[name]; return isRecord(value) ? value : undefined; }; -/** A setting's `config.toml` key and the `SUPABASE_*` env var that overrides it. */ -export interface StackEndpointSetting { - readonly configPath: string; - readonly envVar: string; - /** A deprecated alias of `envVar`, honored only while `envVar` is unset. */ - readonly deprecatedEnvVar?: string; -} - -// Shared by `createCreations` and `stackEndpointSetting`; a new endpoint also needs an entry in -// `endpointSettingsByServiceEndpoint`. -const DB_PORT: StackEndpointSetting = { - envVar: "SUPABASE_DB_PORT", - configPath: "db.port", -}; -const API_PORT: StackEndpointSetting = { - envVar: "SUPABASE_API_PORT", - configPath: "api.port", -}; -const STUDIO_PORT: StackEndpointSetting = { - envVar: "SUPABASE_STUDIO_PORT", - configPath: "studio.port", -}; -const DB_POOLER_PORT: StackEndpointSetting = { - envVar: "SUPABASE_DB_POOLER_PORT", - configPath: "db.pooler.port", -}; -const LOCAL_SMTP_PORT: StackEndpointSetting = { - envVar: "SUPABASE_LOCAL_SMTP_PORT", - configPath: "local_smtp.port", -}; -const LOCAL_SMTP_SMTP_PORT: StackEndpointSetting = { - envVar: "SUPABASE_LOCAL_SMTP_SMTP_PORT", - configPath: "local_smtp.smtp_port", -}; -const LOCAL_SMTP_POP3_PORT: StackEndpointSetting = { - envVar: "SUPABASE_LOCAL_SMTP_POP3_PORT", - configPath: "local_smtp.pop3_port", -}; -const ANALYTICS_PORT: StackEndpointSetting = { - envVar: "SUPABASE_ANALYTICS_PORT", - configPath: "analytics.port", -}; -const EDGE_RUNTIME_INSPECTOR_PORT: StackEndpointSetting = { - envVar: "SUPABASE_EDGE_RUNTIME_INSPECTOR_PORT", - configPath: "edge_runtime.inspector_port", -}; - -/** The configured port when the setting is present in `config.toml` or its env var, else undefined. */ -const resolvePort = Effect.fnUntraced(function* ( - setting: StackEndpointSetting, - document: Readonly> | undefined, - configured: number, - env: Readonly>, -) { - if ((yield* envOverride(setting.envVar, undefined, env)) !== undefined) return configured; - const path = setting.configPath.split("."); - const key = path[path.length - 1] ?? ""; - const parent = path - .slice(0, -1) - .reduce> | undefined>( - (node, name) => section(node, name), - document, - ); - return typeof parent?.[key] === "number" ? configured : undefined; -}); +/** The config key behind a saved stack endpoint; its `path` and `env[0]` name the setting. */ +export type StackEndpointKey = Pick; /** - * Maps a saved stack endpoint (service + endpoint name) to the `StackEndpointSetting` that controls it. + * Maps a saved stack endpoint (service + endpoint name) to the config key that controls it. * An endpoint missing here (e.g. `pooler.http`, `realtime.rpc`) is always automatic. */ -const endpointSettingsByServiceEndpoint: Readonly> = { - "database.sql": DB_PORT, - "pooler.sql": DB_POOLER_PORT, - "analytics.http": ANALYTICS_PORT, - "studio.http": STUDIO_PORT, - "mail.http": LOCAL_SMTP_PORT, - "mail.smtp": LOCAL_SMTP_SMTP_PORT, - "mail.pop3": LOCAL_SMTP_POP3_PORT, - "functions.inspector": EDGE_RUNTIME_INSPECTOR_PORT, - "rest.http": API_PORT, - "auth.http": API_PORT, - "realtime.http": API_PORT, - "storage.http": API_PORT, - "functions.http": API_PORT, -}; - -/** The config.toml key and env var override for a service endpoint, when the CLI exposes one. */ -export const stackEndpointSetting = ( - service: string, - endpoint: string, -): StackEndpointSetting | undefined => endpointSettingsByServiceEndpoint[`${service}.${endpoint}`]; - -/** - * `db.major_version`'s config key and `SUPABASE_DB_MAJOR_VERSION` override. Unlike the ports - * above, `envOverrideMajorVersion` (shared with `db-bootstrap` and the legacy local stack) hardcodes - * its own name/field, so there is no single call site to read this from without widening that - * shared helper's signature; the two literals here are kept in sync by hand. - */ -export const stackMajorVersionSetting: StackEndpointSetting = { - configPath: "db.major_version", - envVar: "SUPABASE_DB_MAJOR_VERSION", +const endpointKeysByServiceEndpoint: Readonly> = { + "database.sql": CliConfigKeys.db.port, + "pooler.sql": CliConfigKeys.db.pooler.port, + "analytics.http": CliConfigKeys.analytics.port, + "studio.http": CliConfigKeys.studio.port, + "mail.http": CliConfigKeys.localSmtp.port, + "mail.smtp": CliConfigKeys.localSmtp.smtpPort, + "mail.pop3": CliConfigKeys.localSmtp.pop3Port, + "functions.inspector": CliConfigKeys.edgeRuntime.inspectorPort, + "rest.http": CliConfigKeys.api.port, + "auth.http": CliConfigKeys.api.port, + "realtime.http": CliConfigKeys.api.port, + "storage.http": CliConfigKeys.api.port, + "functions.http": CliConfigKeys.api.port, }; -/** `db.orioledb_version`'s config key and `SUPABASE_DB_ORIOLEDB_VERSION` override. */ -export const stackOrioledbVersionSetting: StackEndpointSetting = { - configPath: "db.orioledb_version", - envVar: "SUPABASE_DB_ORIOLEDB_VERSION", - deprecatedEnvVar: "SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION", -}; +/** The config key that sets a service endpoint's port, when the CLI exposes one. */ +export const stackEndpointKey = (service: string, endpoint: string): StackEndpointKey | undefined => + endpointKeysByServiceEndpoint[`${service}.${endpoint}`]; -const authProviderNames = [ - "apple", - "azure", - "bitbucket", - "discord", - "facebook", - "github", - "gitlab", - "google", - "kakao", - "keycloak", - "linkedin_oidc", - "notion", - "twitch", - "twitter", - "x", - "slack_oidc", - "spotify", - "workos", - "zoom", -] as const; - -const resolvedPort = Effect.fnUntraced(function* ( - name: string, - configured: number, - field: string, - env: Readonly>, -) { - return (yield* envOverride(name, undefined, env)) === undefined - ? configured - : yield* envOverridePort(name, configured, field, env); -}); - -const resolveAuthOverrides = Effect.fnUntraced(function* ( - auth: CliConfig["auth"], - document: Readonly> | undefined, - env: Readonly>, -) { - const authDocument = section(document, "auth"); - const email = yield* resolveAuthEmail(auth.email, authDocument, env); - const smtp = yield* resolveAuthEmailSmtp(authDocument, env); - const hooks = yield* resolveAuthHooks(authDocument, auth.hook, env); - const external = yield* resolveAuthExternalProviders(authDocument, auth.external, env); - const externalResolved = { ...auth.external }; - for (const name of authProviderNames) { - const value = external[name]; - if (value === undefined) continue; - externalResolved[name] = { - ...externalResolved[name], - enabled: value.enabled, - client_id: value.clientId ?? externalResolved[name].client_id, - secret: value.secret, - url: value.url, - redirect_uri: value.redirectUri ?? externalResolved[name].redirect_uri, - skip_nonce_check: value.skipNonceCheck, - email_optional: value.emailOptional, - }; - } - const figma = external.figma; - if (figma !== undefined) { - externalResolved.figma = { - ...externalResolved.figma, - enabled: figma.enabled, - client_id: figma.clientId ?? externalResolved.figma?.client_id, - secret: figma.secret, - url: figma.url, - redirect_uri: figma.redirectUri ?? externalResolved.figma?.redirect_uri, - skip_nonce_check: figma.skipNonceCheck, - email_optional: figma.emailOptional, - }; - } - const hookValue = ( - value: (typeof hooks)[keyof typeof hooks], - fallback: (typeof auth.hook)[keyof typeof auth.hook], - ) => value ?? fallback; - const hookResolved = { - mfa_verification_attempt: hookValue( - hooks.mfaVerificationAttempt, - auth.hook.mfa_verification_attempt, - ), - password_verification_attempt: hookValue( - hooks.passwordVerificationAttempt, - auth.hook.password_verification_attempt, - ), - custom_access_token: hookValue(hooks.customAccessToken, auth.hook.custom_access_token), - send_sms: hookValue(hooks.sendSms, auth.hook.send_sms), - send_email: hookValue(hooks.sendEmail, auth.hook.send_email), - before_user_created: hookValue(hooks.beforeUserCreated, auth.hook.before_user_created), - }; - const resolvedEmailTemplate: CliConfig["auth"]["email"]["template"] = Object.fromEntries( - Object.entries(email.template).map(([name, value]) => [ - name, - { subject: value.subject ?? "", content_path: value.content_path }, - ]), - ); - const resolvedEmailNotification: CliConfig["auth"]["email"]["notification"] = Object.fromEntries( - Object.entries(email.notification).map(([name, value]) => [ - name, - { - enabled: value.enabled, - subject: value.subject ?? "", - content_path: value.content_path, - }, - ]), - ); - const resolvedEmail = { - ...email, - template: resolvedEmailTemplate, - notification: resolvedEmailNotification, - }; - const additionalRedirectUrls = yield* envOverride( - "SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS", - undefined, - env, - ); - const resolvedSmtp = - smtp === undefined - ? undefined - : { - enabled: smtp.enabled, - host: smtp.host, - ...(smtp.port === 0 ? {} : { port: smtp.port }), - user: smtp.user, - pass: smtp.pass, - admin_email: smtp.adminEmail, - sender_name: smtp.senderName, - }; - const resolvedSms = yield* resolveAuthSms(authDocument, auth.sms, env); - return { - ...auth, - enabled: yield* envOverrideBool("SUPABASE_AUTH_ENABLED", auth.enabled, "auth.enabled", env), - site_url: yield* envOverride("SUPABASE_AUTH_SITE_URL", auth.site_url, env), - additional_redirect_urls: - additionalRedirectUrls === undefined - ? auth.additional_redirect_urls - : strToArr(additionalRedirectUrls), - jwt_expiry: yield* envOverrideUint( - "SUPABASE_AUTH_JWT_EXPIRY", - "auth.jwt_expiry", - auth.jwt_expiry, - env, - ), - enable_refresh_token_rotation: yield* envOverrideBool( - "SUPABASE_AUTH_ENABLE_REFRESH_TOKEN_ROTATION", - auth.enable_refresh_token_rotation, - "auth.enable_refresh_token_rotation", - env, - ), - refresh_token_reuse_interval: yield* envOverrideUint( - "SUPABASE_AUTH_REFRESH_TOKEN_REUSE_INTERVAL", - "auth.refresh_token_reuse_interval", - auth.refresh_token_reuse_interval, - env, - ), - enable_manual_linking: yield* envOverrideBool( - "SUPABASE_AUTH_ENABLE_MANUAL_LINKING", - auth.enable_manual_linking, - "auth.enable_manual_linking", - env, - ), - enable_signup: yield* envOverrideBool( - "SUPABASE_AUTH_ENABLE_SIGNUP", - auth.enable_signup, - "auth.enable_signup", - env, - ), - enable_anonymous_sign_ins: yield* envOverrideBool( - "SUPABASE_AUTH_ENABLE_ANONYMOUS_SIGN_INS", - auth.enable_anonymous_sign_ins, - "auth.enable_anonymous_sign_ins", - env, - ), - minimum_password_length: yield* envOverrideUint( - "SUPABASE_AUTH_MINIMUM_PASSWORD_LENGTH", - "auth.minimum_password_length", - auth.minimum_password_length, - env, - ), - password_requirements: yield* envOverrideAuthPasswordRequirements( - auth.password_requirements, - env, - ), - publishable_key: yield* envOverride("SUPABASE_AUTH_PUBLISHABLE_KEY", auth.publishable_key, env), - secret_key: yield* envOverride("SUPABASE_AUTH_SECRET_KEY", auth.secret_key, env), - anon_key: yield* envOverride("SUPABASE_AUTH_ANON_KEY", auth.anon_key, env), - service_role_key: yield* envOverride( - "SUPABASE_AUTH_SERVICE_ROLE_KEY", - auth.service_role_key, - env, - ), - rate_limit: yield* resolveGotrueRateLimit(auth.rate_limit, env), - captcha: yield* resolveAuthCaptcha(authDocument, auth.captcha, env), - hook: hookResolved, - mfa: yield* resolveAuthMfa(auth.mfa, env), - sessions: yield* resolveGotrueSessions(auth.sessions, env), - email: { ...resolvedEmail, smtp: resolvedSmtp }, - sms: { - ...resolvedSms, - twilio: { - ...resolvedSms.twilio, - content_sid: yield* envOverride( - "SUPABASE_AUTH_SMS_TWILIO_CONTENT_SID", - auth.sms.twilio.content_sid, - env, - ), - }, - }, - external: externalResolved, - web3: yield* resolveGotrueWeb3(auth.web3, env), - oauth_server: yield* resolveGotrueOAuthServer(auth.oauth_server, env), - }; -}); - -const resolveEffectiveCliConfig = Effect.fnUntraced(function* ( - config: CliConfig, - document: Readonly> | undefined, - env: Readonly>, -) { - const api = config.api; - const db = config.db; - const storage = config.storage; - const realtime = config.realtime; - const analytics = config.analytics; - const studio = config.studio; - const mail = config.local_smtp; - const pooler = db.pooler; - const edge = config.edge_runtime; - const experimental = config.experimental; - const apiSchemasOverride = yield* envOverride("SUPABASE_API_SCHEMAS", undefined, env); - const apiExtraSearchPathOverride = yield* envOverride( - "SUPABASE_API_EXTRA_SEARCH_PATH", - undefined, - env, - ); - const imageDocument = section(section(document, "storage"), "image_transformation"); - const imageEnabledOverride = yield* envOverride( - "SUPABASE_STORAGE_IMAGE_TRANSFORMATION_ENABLED", - undefined, - env, - ); - const imageEnabledExplicit = - imageDocument?.enabled !== undefined || imageEnabledOverride !== undefined; - const resolvedApi = { - ...api, - enabled: yield* envOverrideBool("SUPABASE_API_ENABLED", api.enabled, "api.enabled", env), - port: yield* resolvedPort("SUPABASE_API_PORT", api.port, "api.port", env), - schemas: apiSchemasOverride === undefined ? api.schemas : strToArr(apiSchemasOverride), - extra_search_path: - apiExtraSearchPathOverride === undefined - ? api.extra_search_path - : strToArr(apiExtraSearchPathOverride), - max_rows: yield* envOverrideApiMaxRows(api.max_rows, env), - tls: { - ...api.tls, - enabled: yield* envOverrideBool( - "SUPABASE_API_TLS_ENABLED", - api.tls.enabled, - "api.tls.enabled", - env, - ), - cert_path: yield* envOverride("SUPABASE_API_TLS_CERT_PATH", api.tls.cert_path, env), - key_path: yield* envOverride("SUPABASE_API_TLS_KEY_PATH", api.tls.key_path, env), - }, - external_url: yield* envOverride("SUPABASE_API_EXTERNAL_URL", api.external_url, env), - auto_expose_new_tables: - api.auto_expose_new_tables !== undefined || - (yield* envOverride("SUPABASE_API_AUTO_EXPOSE_NEW_TABLES", undefined, env)) !== undefined - ? yield* envOverrideBool( - "SUPABASE_API_AUTO_EXPOSE_NEW_TABLES", - api.auto_expose_new_tables ?? false, - "api.auto_expose_new_tables", - env, - ) - : undefined, - }; - const resolvedStorage = { - ...storage, - enabled: yield* envOverrideBool( - "SUPABASE_STORAGE_ENABLED", - storage.enabled, - "storage.enabled", - env, - ), - file_size_limit: yield* envOverride( - "SUPABASE_STORAGE_FILE_SIZE_LIMIT", - String(storage.file_size_limit), - env, - ), - image_transformation: imageEnabledExplicit - ? { - ...storage.image_transformation, - enabled: yield* envOverrideBool( - "SUPABASE_STORAGE_IMAGE_TRANSFORMATION_ENABLED", - storage.image_transformation?.enabled ?? false, - "storage.image_transformation.enabled", - env, - ), - } - : undefined, - s3_protocol: { - ...storage.s3_protocol, - enabled: yield* envOverrideBool( - "SUPABASE_STORAGE_S3_PROTOCOL_ENABLED", - storage.s3_protocol.enabled, - "storage.s3_protocol.enabled", - env, - ), - }, - analytics: { - ...storage.analytics, - enabled: yield* envOverrideBool( - "SUPABASE_STORAGE_ANALYTICS_ENABLED", - storage.analytics.enabled, - "storage.analytics.enabled", - env, - ), - max_namespaces: yield* envOverrideUint( - "SUPABASE_STORAGE_ANALYTICS_MAX_NAMESPACES", - "storage.analytics.max_namespaces", - storage.analytics.max_namespaces, - env, - ), - max_tables: yield* envOverrideUint( - "SUPABASE_STORAGE_ANALYTICS_MAX_TABLES", - "storage.analytics.max_tables", - storage.analytics.max_tables, - env, - ), - max_catalogs: yield* envOverrideUint( - "SUPABASE_STORAGE_ANALYTICS_MAX_CATALOGS", - "storage.analytics.max_catalogs", - storage.analytics.max_catalogs, - env, - ), - }, - vector: { - ...storage.vector, - enabled: yield* envOverrideBool( - "SUPABASE_STORAGE_VECTOR_ENABLED", - storage.vector.enabled, - "storage.vector.enabled", - env, - ), - max_buckets: yield* envOverrideUint( - "SUPABASE_STORAGE_VECTOR_MAX_BUCKETS", - "storage.vector.max_buckets", - storage.vector.max_buckets, - env, - ), - max_indexes: yield* envOverrideUint( - "SUPABASE_STORAGE_VECTOR_MAX_INDEXES", - "storage.vector.max_indexes", - storage.vector.max_indexes, - env, - ), - }, - }; - const resolvedEdge = { - ...edge, - enabled: yield* envOverrideBool( - "SUPABASE_EDGE_RUNTIME_ENABLED", - edge.enabled, - "edge_runtime.enabled", - env, - ), - policy: yield* envOverrideEdgeRuntimePolicy(edge.policy, env), - deno_version: yield* envOverrideDenoVersion(edge.deno_version, env), - inspector_port: yield* resolvedPort( - "SUPABASE_EDGE_RUNTIME_INSPECTOR_PORT", - edge.inspector_port, - "edge_runtime.inspector_port", - env, - ), - }; - const resolvedAnalytics = { - ...analytics, - enabled: yield* envOverrideBool( - "SUPABASE_ANALYTICS_ENABLED", - analytics.enabled, - "analytics.enabled", - env, - ), - port: yield* resolvedPort("SUPABASE_ANALYTICS_PORT", analytics.port, "analytics.port", env), - backend: yield* envOverrideAnalyticsBackend(analytics.backend, env), - gcp_project_id: yield* envOverride( - "SUPABASE_ANALYTICS_GCP_PROJECT_ID", - analytics.gcp_project_id, - env, - ), - gcp_project_number: yield* envOverride( - "SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER", - analytics.gcp_project_number, - env, - ), - gcp_jwt_path: yield* envOverride( - "SUPABASE_ANALYTICS_GCP_JWT_PATH", - analytics.gcp_jwt_path, - env, - ), - }; - const resolvedPooler = { - ...pooler, - enabled: yield* envOverrideBool( - "SUPABASE_DB_POOLER_ENABLED", - pooler.enabled, - "db.pooler.enabled", - env, - ), - port: yield* resolvedPort("SUPABASE_DB_POOLER_PORT", pooler.port, "db.pooler.port", env), - pool_mode: yield* envOverridePoolMode(pooler.pool_mode, env), - default_pool_size: yield* envOverrideDefaultPoolSize(pooler.default_pool_size, env), - max_client_conn: yield* envOverrideMaxClientConn(pooler.max_client_conn, env), - }; - const authEnabled = yield* envOverrideBool( - "SUPABASE_AUTH_ENABLED", - config.auth.enabled, - "auth.enabled", - env, - ); - const thirdParty = { - firebase: { - enabled: yield* envOverrideBool( - "SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED", - config.auth.third_party.firebase.enabled, - "auth.third_party.firebase.enabled", - env, - ), - project_id: yield* envOverride( - "SUPABASE_AUTH_THIRD_PARTY_FIREBASE_PROJECT_ID", - config.auth.third_party.firebase.project_id, - env, - ), - }, - auth0: { - enabled: yield* envOverrideBool( - "SUPABASE_AUTH_THIRD_PARTY_AUTH0_ENABLED", - config.auth.third_party.auth0.enabled, - "auth.third_party.auth0.enabled", - env, - ), - tenant: yield* envOverride( - "SUPABASE_AUTH_THIRD_PARTY_AUTH0_TENANT", - config.auth.third_party.auth0.tenant, - env, - ), - tenant_region: yield* envOverride( - "SUPABASE_AUTH_THIRD_PARTY_AUTH0_TENANT_REGION", - config.auth.third_party.auth0.tenant_region, - env, - ), - }, - aws_cognito: { - enabled: yield* envOverrideBool( - "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_ENABLED", - config.auth.third_party.aws_cognito.enabled, - "auth.third_party.aws_cognito.enabled", - env, - ), - user_pool_id: yield* envOverride( - "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_USER_POOL_ID", - config.auth.third_party.aws_cognito.user_pool_id, - env, - ), - user_pool_region: yield* envOverride( - "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_USER_POOL_REGION", - config.auth.third_party.aws_cognito.user_pool_region, - env, - ), - }, - clerk: { - enabled: yield* envOverrideBool( - "SUPABASE_AUTH_THIRD_PARTY_CLERK_ENABLED", - config.auth.third_party.clerk.enabled, - "auth.third_party.clerk.enabled", - env, - ), - domain: yield* envOverride( - "SUPABASE_AUTH_THIRD_PARTY_CLERK_DOMAIN", - config.auth.third_party.clerk.domain, - env, - ), - }, - workos: { - enabled: yield* envOverrideBool( - "SUPABASE_AUTH_THIRD_PARTY_WORKOS_ENABLED", - config.auth.third_party.workos.enabled, - "auth.third_party.workos.enabled", - env, - ), - issuer_url: yield* envOverride( - "SUPABASE_AUTH_THIRD_PARTY_WORKOS_ISSUER_URL", - config.auth.third_party.workos.issuer_url, - env, - ), - }, - }; - // JWT security settings apply to non-Auth workloads too, so resolve them - // regardless of whether the Auth capability is enabled. - const authResolved = { - ...(authEnabled ? yield* resolveAuthOverrides(config.auth, document, env) : config.auth), - enabled: authEnabled, - third_party: thirdParty, - jwt_issuer: yield* envOverride("SUPABASE_AUTH_JWT_ISSUER", config.auth.jwt_issuer, env), - signing_keys_path: yield* envOverride( - "SUPABASE_AUTH_SIGNING_KEYS_PATH", - config.auth.signing_keys_path, - env, - ), - jwt_secret: yield* decryptAuthSecret( - yield* envOverride("SUPABASE_AUTH_JWT_SECRET", config.auth.jwt_secret, env), - env, - ), - }; - return { - ...config, - api: resolvedApi, - auth: authResolved, - db: { - ...db, - port: yield* resolvedPort("SUPABASE_DB_PORT", db.port, "db.port", env), - major_version: yield* envOverrideMajorVersion(db.major_version, env), - health_timeout: yield* envOverride("SUPABASE_DB_HEALTH_TIMEOUT", db.health_timeout, env), - orioledb_version: yield* envOverrideOrioleDBVersion(db.orioledb_version, env), - settings: yield* resolveDbSettingsEnvOverrides(db.settings, env), - pooler: resolvedPooler, - }, - edge_runtime: resolvedEdge, - // Same S3 overrides legacy Compose applies, so the OrioleDB S3 guard sees env-only values. - experimental: { - ...experimental, - s3_host: yield* envOverride("SUPABASE_EXPERIMENTAL_S3_HOST", experimental.s3_host, env), - s3_region: yield* envOverride("SUPABASE_EXPERIMENTAL_S3_REGION", experimental.s3_region, env), - s3_access_key: yield* envOverride( - "SUPABASE_EXPERIMENTAL_S3_ACCESS_KEY", - experimental.s3_access_key, - env, - ), - s3_secret_key: yield* envOverride( - "SUPABASE_EXPERIMENTAL_S3_SECRET_KEY", - experimental.s3_secret_key, - env, - ), - }, - realtime: { - ...realtime, - enabled: yield* envOverrideBool( - "SUPABASE_REALTIME_ENABLED", - realtime.enabled, - "realtime.enabled", - env, - ), - ip_version: yield* envOverrideRealtimeIpVersion(realtime.ip_version, env), - max_header_length: yield* envOverrideRealtimeMaxHeaderLength(realtime.max_header_length, env), - }, - storage: resolvedStorage, - analytics: resolvedAnalytics, - studio: { - ...studio, - enabled: yield* envOverrideBool( - "SUPABASE_STUDIO_ENABLED", - studio.enabled, - "studio.enabled", - env, - ), - port: yield* resolvedPort("SUPABASE_STUDIO_PORT", studio.port, "studio.port", env), - api_url: yield* envOverride("SUPABASE_STUDIO_API_URL", studio.api_url, env), - openai_api_key: yield* decryptAuthSecret( - yield* envOverride("SUPABASE_STUDIO_OPENAI_API_KEY", studio.openai_api_key, env), - env, - ), - }, - local_smtp: { - ...mail, - enabled: yield* envOverrideBool( - "SUPABASE_LOCAL_SMTP_ENABLED", - mail.enabled, - "local_smtp.enabled", - env, - ), - port: yield* resolvedPort("SUPABASE_LOCAL_SMTP_PORT", mail.port, "local_smtp.port", env), - smtp_port: yield* resolvedPort( - "SUPABASE_LOCAL_SMTP_SMTP_PORT", - mail.smtp_port ?? 0, - "local_smtp.smtp_port", - env, - ), - pop3_port: yield* resolvedPort( - "SUPABASE_LOCAL_SMTP_POP3_PORT", - mail.pop3_port ?? 0, - "local_smtp.pop3_port", - env, - ), - admin_email: yield* envOverride("SUPABASE_LOCAL_SMTP_ADMIN_EMAIL", mail.admin_email, env), - sender_name: yield* envOverride("SUPABASE_LOCAL_SMTP_SENDER_NAME", mail.sender_name, env), - }, - } satisfies CliConfig; -}); +export const stackMajorVersionKey: StackEndpointKey = CliConfigKeys.db.majorVersion; const unsupportedConfigPaths = [ { path: "api.tls", active: (config: CliConfig) => config.api.enabled }, @@ -965,6 +260,9 @@ const configValidationError = (config: CliConfig): string | undefined => { return undefined; }; +const toStackConfigError = (cause: { readonly message: string }) => + new StackConfigError({ message: cause.message }); + const endpoint = (port: number | undefined): { readonly port: number | "auto" } => ({ port: port === undefined ? "auto" : port, }); @@ -976,38 +274,20 @@ export const loadStackConfig = Effect.fn("StackConfig.load")( opts?: StackConfigOptions, ): StackConfigEffect => Effect.gen(function* () { - const context = - opts?.context ?? - (yield* loadLocalProjectContext( - projectRoot, - (message) => new StackConfigError({ message }), - )); - const effectiveInput = yield* resolveEffectiveCliConfig( - context.config, - context.loaded?.document, - context.projectEnvValues, + const { + resolvedConfig, + config: validatedConfig, + projectEnvValues, + document, + } = yield* loadResolvedConfigContext( + projectRoot, + Option.fromNullishOr(opts?.projectRef), ).pipe( Effect.mapError( - (cause) => - new StackConfigError({ - message: cause instanceof Error ? cause.message : "invalid config overrides", - }), + (cause) => new StackConfigError({ message: describeConfigLoadFailure(cause) }), ), ); - const validatedConfig = yield* validateCliConfig(withoutUndefined(effectiveInput)).pipe( - Effect.mapError((cause) => { - const issues = SchemaIssue.makeFormatterStandardSchemaV1({ - leafHook: () => "Invalid value", - checkHook: () => undefined, - })(cause.issue).issues; - const path = issues[0]?.path - ?.map((segment) => String(typeof segment === "object" ? segment.key : segment)) - .join("."); - return new StackConfigError({ - message: path === undefined ? "invalid config" : `invalid config at ${path}`, - }); - }), - ); + const { originAt } = resolvedConfig.materialized; yield* recordOrioleDbTelemetry( validatedConfig.db.orioledb_version, validatedConfig.db.major_version, @@ -1015,34 +295,32 @@ export const loadStackConfig = Effect.fn("StackConfig.load")( const validationError = configValidationError(validatedConfig); if (validationError !== undefined) return yield* new StackConfigError({ message: validationError }); - if (context.loaded === null) + if (!resolvedConfig.hasConfigFile) yield* rejectFrozenWithoutConfig(projectRoot, opts?.frozen === true); - const externalProviders = yield* resolveAuthExternalProviders( - section(context.loaded?.document, "auth"), - validatedConfig.auth.external, - context.projectEnvValues, - ).pipe( - Effect.mapError( - (cause) => - new StackConfigError({ - message: cause instanceof Error ? cause.message : "invalid auth provider config", - }), - ), + const externalProviders = yield* Effect.try({ + try: () => + resolveAuthExternalProviders(section(document, "auth"), validatedConfig.auth.external), + catch: (cause) => + new StackConfigError({ + message: cause instanceof Error ? cause.message : "invalid auth provider config", + }), + }); + const authExternalUrl = Option.getOrUndefined( + (yield* resolvedConfig + .get(CliConfigKeys.auth.externalUrl) + .pipe(Effect.mapError(toStackConfigError))).value, ); const authConfig = yield* resolveAuthConfig( validatedConfig.auth, validatedConfig.local_smtp, { - authExternalUrl: yield* resolveAuthExternalUrl( - context.loaded?.document, - context.projectEnvValues, - ), + smtpEnabled: resolveSmtpEnabled(resolvedConfig), + authExternalUrl, apiExternalUrl: validatedConfig.api.external_url, externalProviders, - ...(yield* resolveGotruePasskeyWebauthn( - context.loaded?.document, - context.projectEnvValues, - ).pipe(Effect.orDie)), + ...(yield* resolvePasskeyWebauthn(resolvedConfig).pipe( + Effect.mapError(toStackConfigError), + )), }, ); const path = yield* Path.Path; @@ -1060,7 +338,7 @@ export const loadStackConfig = Effect.fn("StackConfig.load")( message: cause instanceof Error ? cause.message : String(cause), }), }); - const toStackConfigError = (cause: unknown) => + const stackKeyError = (cause: unknown) => cause instanceof StackConfigError ? cause : new StackConfigError({ @@ -1068,19 +346,15 @@ export const loadStackConfig = Effect.fn("StackConfig.load")( }); const localKeys = yield* Effect.cached( Effect.gen(function* () { - const configured = Effect.fnUntraced(function* (value: string | undefined) { - return value === undefined || value === "" - ? undefined - : yield* decryptAuthSecret(value, context.projectEnvValues); - }); - const publishableKey = yield* configured(auth.publishable_key); - const secretKey = yield* configured(auth.secret_key); - const configuredAnonKey = yield* configured(auth.anon_key); - const configuredServiceRoleKey = yield* configured(auth.service_role_key); + const configured = (value: string | undefined) => + value === undefined || value === "" ? undefined : value; + const publishableKey = configured(auth.publishable_key); + const secretKey = configured(auth.secret_key); + const configuredAnonKey = configured(auth.anon_key); + const configuredServiceRoleKey = configured(auth.service_role_key); const configuredSigningKeys = yield* resolveConfiguredSigningKeys( validatedConfig, projectRoot, - context.projectEnvValues, ); const signingKeys = configuredSigningKeys ?? @@ -1095,12 +369,12 @@ export const loadStackConfig = Effect.fn("StackConfig.load")( ...(configuredAnonKey === undefined ? signingKey === undefined ? {} - : { anonKey: generateAsymmetricGoJwt(signingKey, "anon") } + : { anonKey: generateAsymmetricLocalJwt(signingKey, "anon") } : { anonKey: configuredAnonKey }), ...(configuredServiceRoleKey === undefined ? signingKey === undefined ? {} - : { serviceRoleKey: generateAsymmetricGoJwt(signingKey, "service_role") } + : { serviceRoleKey: generateAsymmetricLocalJwt(signingKey, "service_role") } : { serviceRoleKey: configuredServiceRoleKey }), anonKeyIsOverride: configuredAnonKey !== undefined, serviceRoleKeyIsOverride: configuredServiceRoleKey !== undefined, @@ -1111,10 +385,10 @@ export const loadStackConfig = Effect.fn("StackConfig.load")( publicSigningKeys: encodeJwkArray(signingKeys.map(toPublicJwk)), }), }), - catch: toStackConfigError, + catch: stackKeyError, }); }).pipe( - Effect.mapError(toStackConfigError), + Effect.mapError(stackKeyError), Effect.provideService(FileSystem.FileSystem, yield* FileSystem.FileSystem), Effect.provideService(Path.Path, path), ), @@ -1140,12 +414,8 @@ export const loadStackConfig = Effect.fn("StackConfig.load")( }); const functionEnvironments = Object.fromEntries( yield* Effect.forEach(Object.entries(validatedConfig.functions), ([name, config]) => - resolveCliConfigSubtree( - config.env, - { values: context.projectEnvValues }, - `functions.${name}.env`, - { goViperCompat: true }, - ).pipe( + resolveConfigSubtree(resolvedConfig, config.env, `functions.${name}.env`).pipe( + Effect.mapError((error) => new StackConfigError({ message: error.message })), Effect.map( (env) => [ @@ -1197,95 +467,64 @@ export const loadStackConfig = Effect.fn("StackConfig.load")( ), catch: (cause) => new StackConfigError({ message: String(cause) }), }); - const functionsEnv = Object.fromEntries( - yield* Effect.forEach( - Object.entries(validatedConfig.edge_runtime.secrets ?? {}), - ([key, value]) => - decryptAuthSecret(value, context.projectEnvValues).pipe( - Effect.map((decrypted) => [key, decrypted ?? ""] as const), - ), - ).pipe(Effect.mapError((cause) => new StackConfigError({ message: String(cause) }))), + const functionsEnvResult = decryptSecretMap( + Object.fromEntries( + Object.entries(validatedConfig.edge_runtime.secrets ?? {}).map(([key, value]) => [ + key, + value ?? "", + ]), + ), + resolvedConfig.dotenvPrivateKeys, ); + if (!functionsEnvResult.ok) + return yield* new StackConfigError({ + message: `failed to parse config: ${functionsEnvResult.error}`, + }); + const functionsEnv = functionsEnvResult.value; const storageFileSizeLimit = yield* Effect.try({ try: () => String(parseFileSizeLimit(validatedConfig.storage.file_size_limit)), catch: (cause) => new StackConfigError({ message: `Invalid storage.file_size_limit: ${String(cause)}` }), }); const healthTimeoutMs = yield* Effect.try({ - try: () => parseGoDuration(validatedConfig.db.health_timeout) / 1_000_000, + try: () => parseDuration(validatedConfig.db.health_timeout) / 1_000_000, catch: (cause) => new StackConfigError({ message: `Invalid db.health_timeout: ${String(cause)}` }), }); - const configuredJwtSecret = - validatedConfig.auth.jwt_secret === undefined || validatedConfig.auth.jwt_secret === "" - ? undefined - : yield* resolveJwtSecret(validatedConfig.auth.jwt_secret).pipe( - Effect.mapError((cause) => new StackConfigError({ message: String(cause) })), - ); + const configuredJwtSecret = yield* Effect.try({ + try: () => + validatedConfig.auth.jwt_secret === undefined || validatedConfig.auth.jwt_secret === "" + ? undefined + : resolveJwtSecret(validatedConfig.auth.jwt_secret), + catch: (cause) => new StackConfigError({ message: String(cause) }), + }); const jwtSecret = configuredJwtSecret === undefined ? undefined : Redacted.make(configuredJwtSecret); - const document = context.loaded?.document; - const rootKey = yield* Effect.gen(function* () { - const raw = section(document, "db")?.root_key; - if (raw !== undefined && typeof raw !== "string") - return yield* new StackConfigError({ message: "Error: db.root_key must be a string" }); - const value = yield* decryptAuthSecret( - yield* envOverride("SUPABASE_DB_ROOT_KEY", raw, context.projectEnvValues), - context.projectEnvValues, - ).pipe(Effect.mapError((cause) => new StackConfigError({ message: String(cause) }))); - return value === undefined || value === "" ? undefined : value; - }); - const dbPort = yield* resolvePort( - DB_PORT, - document, - validatedConfig.db.port, - context.projectEnvValues, - ); - const apiPort = yield* resolvePort( - API_PORT, - document, - validatedConfig.api.port, - context.projectEnvValues, - ); - const studioPort = yield* resolvePort( - STUDIO_PORT, - document, - validatedConfig.studio.port, - context.projectEnvValues, - ); - const poolerPort = yield* resolvePort( - DB_POOLER_PORT, - document, - validatedConfig.db.pooler.port, - context.projectEnvValues, - ); - const mailPort = yield* resolvePort( - LOCAL_SMTP_PORT, - document, - validatedConfig.local_smtp.port, - context.projectEnvValues, + const rootKey = yield* resolvedConfig.get(CliConfigKeys.db.rootKey).pipe( + Effect.map(({ value, origin }) => + origin.tier === "default" || value === "" ? undefined : value, + ), + Effect.mapError(toStackConfigError), ); - const mailSmtpPort = yield* resolvePort( - LOCAL_SMTP_SMTP_PORT, - document, + const configuredPort = (path: string, port: number) => + originAt(path).tier === "default" ? undefined : port; + const dbPort = configuredPort("db.port", validatedConfig.db.port); + const apiPort = configuredPort("api.port", validatedConfig.api.port); + const studioPort = configuredPort("studio.port", validatedConfig.studio.port); + const poolerPort = configuredPort("db.pooler.port", validatedConfig.db.pooler.port); + const mailPort = configuredPort("local_smtp.port", validatedConfig.local_smtp.port); + const mailSmtpPort = configuredPort( + "local_smtp.smtp_port", validatedConfig.local_smtp.smtp_port ?? 0, - context.projectEnvValues, ); - const mailPop3Port = yield* resolvePort( - LOCAL_SMTP_POP3_PORT, - document, + const mailPop3Port = configuredPort( + "local_smtp.pop3_port", validatedConfig.local_smtp.pop3_port ?? 0, - context.projectEnvValues, - ); - const analyticsPort = yield* resolvePort( - ANALYTICS_PORT, - document, - validatedConfig.analytics.port, - context.projectEnvValues, ); + const analyticsPort = configuredPort("analytics.port", validatedConfig.analytics.port); const poolMode = validatedConfig.db.pooler.pool_mode === "session" ? ("session" as const) : "transaction"; - const selection = yield* context.loaded !== null && opts?.selection !== undefined + const selection = yield* resolvedConfig.hasConfigFile && opts?.selection !== undefined ? (opts.publishesLock === true ? localKeys : Effect.void).pipe( Effect.andThen( opts.selection({ @@ -1308,7 +547,7 @@ export const loadStackConfig = Effect.fn("StackConfig.load")( const createCreations = ( stackId: string, ): Effect.Effect, StackConfigError> => - Effect.gen(function* () { + Effect.sync(() => { return [ { service: "database", @@ -1451,11 +690,9 @@ export const loadStackConfig = Effect.fn("StackConfig.load")( endpoints: { http: endpoint(apiPort), inspector: endpoint( - yield* resolvePort( - EDGE_RUNTIME_INSPECTOR_PORT, - document, + configuredPort( + "edge_runtime.inspector_port", validatedConfig.edge_runtime.inspector_port, - context.projectEnvValues, ), ), }, @@ -1505,10 +742,10 @@ export const loadStackConfig = Effect.fn("StackConfig.load")( selection, creations: createCreations, source: validatedConfig, - projectEnvValues: context.projectEnvValues, + projectEnvValues, + originAt, remoteJwks, keys, - ...(context.loaded?.document === undefined ? {} : { document: context.loaded.document }), }; }), ); diff --git a/apps/cli/src/command-internal/stack-functions-env.ts b/apps/cli/src/command-internal/stack-functions-env.ts index 27f55fe62f..4d7c33290e 100644 --- a/apps/cli/src/command-internal/stack-functions-env.ts +++ b/apps/cli/src/command-internal/stack-functions-env.ts @@ -1,6 +1,6 @@ import { Data, Effect, FileSystem, Predicate } from "effect"; import type { ServiceCreationInput } from "@supabase/stack/effect"; -import { parseDotEnv } from "./dotenv.ts"; +import { parseDotEnv } from "../shared/config/dotenv.ts"; import { actionability, type CliErrorActionabilityDeclaration, diff --git a/apps/cli/src/command-internal/stack-lock.ts b/apps/cli/src/command-internal/stack-lock.ts index b44b79db70..2cd70b06bc 100644 --- a/apps/cli/src/command-internal/stack-lock.ts +++ b/apps/cli/src/command-internal/stack-lock.ts @@ -41,7 +41,7 @@ import { } from "effect"; import { FetchHttpClient } from "effect/http"; -import { ensureGitignore, readProjectLink } from "./project-link.ts"; +import { ensureGitignore, readProjectLink } from "../shared/config/project-link.ts"; import { stackArtifactSelection } from "./stack-artifact-selection.ts"; import { StackConfigError } from "./stack-config.ts"; import { warnDataConflicts } from "./stack-data-conflicts.ts"; diff --git a/apps/cli/src/command-internal/stack-shadow-cache.ts b/apps/cli/src/command-internal/stack-shadow-cache.ts index a28669d8b9..6c49ddfddd 100644 --- a/apps/cli/src/command-internal/stack-shadow-cache.ts +++ b/apps/cli/src/command-internal/stack-shadow-cache.ts @@ -7,7 +7,7 @@ import { type ShadowCacheKeyInputs, } from "./db-bootstrap/shadow-cache.ts"; import { resolveSetupWebhooksEnabled, type SetupDatabaseOptions } from "./db-bootstrap/db-setup.ts"; -import { viperEnvBoolWithProjectFallback } from "./viper-env.ts"; +import { supabaseEnvBoolWithProjectFallback } from "./supabase-env.ts"; const encodeKeyParts = Schema.encodeEffect(Schema.fromJsonString(Schema.Array(Schema.String))); @@ -38,9 +38,13 @@ export const stackShadowCacheEntry = Effect.fn("StackShadowCache.entry")(functio ) { if ( bypassCache || - !(yield* viperEnvBoolWithProjectFallback(SHADOW_CACHE_ENV, input.setup.projectEnvValues ?? {}, { - whenUnset: true, - })) + !(yield* supabaseEnvBoolWithProjectFallback( + SHADOW_CACHE_ENV, + input.setup.projectEnvValues ?? {}, + { + whenUnset: true, + }, + )) ) return undefined; const rolesSql = yield* readRoles(input); diff --git a/apps/cli/src/command-internal/stack-shadow.integration.test.ts b/apps/cli/src/command-internal/stack-shadow.integration.test.ts index 3c75e7cd31..d16481ab95 100644 --- a/apps/cli/src/command-internal/stack-shadow.integration.test.ts +++ b/apps/cli/src/command-internal/stack-shadow.integration.test.ts @@ -10,6 +10,7 @@ import { describe, expect, it } from "@effect/vitest"; import { Deferred, Effect, FileSystem, Layer, Option, Path, Ref, Schema } from "effect"; import { mockOutput } from "../../tests/helpers/mocks.ts"; import { mockCommandSettings } from "../../tests/helpers/command-mocks.ts"; +import { cliConfigValuesTestLayer } from "../../tests/helpers/config-values-layer.ts"; import { runtimeInfoLayer } from "../shared/runtime/runtime-info.layer.ts"; import { DbConnection } from "./db-connection.service.ts"; import { dbConnectionLayer } from "./db-connection.sql-pg.layer.ts"; @@ -666,6 +667,7 @@ describe("stack shadow databases", () => { const settings = Layer.mergeAll( runtimeInfoLayer, mockCommandSettings({ workdir: root, supabaseHome: root }), + cliConfigValuesTestLayer, ); const locked = unbundled(selection.database); yield* seedLock(root, (services) => ({ @@ -675,7 +677,7 @@ describe("stack shadow databases", () => { const context = yield* loadLocalProjectContext(root, (message) => message, ref).pipe( Effect.provide(settings), ); - expect(context.loaded?.appliedRemote).toBeDefined(); + expect(Option.isSome(context.resolvedConfig.appliedRemote)).toBe(true); expect(context.config.db.major_version).toBe(remote.major); expect(context.config.db.orioledb_version).toBe(remote.db.orioledb_version); diff --git a/apps/cli/src/command-internal/stack-shadow.ts b/apps/cli/src/command-internal/stack-shadow.ts index 442d62a99c..faa3ce1620 100644 --- a/apps/cli/src/command-internal/stack-shadow.ts +++ b/apps/cli/src/command-internal/stack-shadow.ts @@ -1,4 +1,4 @@ -import { Effect, FileSystem, Path, Redacted, Result, Schema } from "effect"; +import { Effect, FileSystem, Option, Path, Redacted, Result, Schema } from "effect"; import { type DatabaseInstance, type Stack } from "@supabase/stack/effect"; import type { ArtifactPin, ArtifactSelection } from "@supabase/stack/internal/artifacts"; import { CommandSettings } from "../config/command-settings.service.ts"; @@ -241,7 +241,10 @@ export const stackAcquireShadowDatabase = Effect.fn("StackShadow.acquire")(funct majorVersion: input.setup.majorVersion, orioledbVersion: input.db.orioledb_version, services: input.context?.config.local?.services, - appliedRemote: input.context?.loaded?.appliedRemote, + appliedRemote: + input.context === undefined + ? undefined + : Option.getOrUndefined(input.context.resolvedConfig.appliedRemote), }).pipe( Effect.mapError( (error) => diff --git a/apps/cli/src/command-internal/stack-storage.native.integration.test.ts b/apps/cli/src/command-internal/stack-storage.native.integration.test.ts index 1c29a13e9d..77f0170d10 100644 --- a/apps/cli/src/command-internal/stack-storage.native.integration.test.ts +++ b/apps/cli/src/command-internal/stack-storage.native.integration.test.ts @@ -28,6 +28,7 @@ import { runtimeInfoLayer } from "../shared/runtime/runtime-info.layer.ts"; import { ExperimentalFlag, YesFlag } from "./global-flags.ts"; import { stackStart } from "../commands/experimental/stack/start/start.handler.ts"; import { destroyTestStacks } from "../../tests/helpers/stack-cleanup.ts"; +import { cliConfigValuesTestLayer } from "../../tests/helpers/config-values-layer.ts"; const projectConfig = ` project_id = "stack-storage-native-integration" @@ -80,6 +81,7 @@ const makeLayers = (root: string) => { const telemetry = mockTelemetryStateTracked(); const tty = mockTty({ stdinIsTty: false, stdoutIsTty: false }); return Layer.mergeAll( + cliConfigValuesTestLayer, FetchHttpClient.layer, runtimeInfoLayer, settings, diff --git a/apps/cli/src/command-internal/start-secrets-cleanup.ts b/apps/cli/src/command-internal/start-secrets-cleanup.ts index daf10a24cb..bca8b82bee 100644 --- a/apps/cli/src/command-internal/start-secrets-cleanup.ts +++ b/apps/cli/src/command-internal/start-secrets-cleanup.ts @@ -1,6 +1,6 @@ import { Effect, FileSystem, Path } from "effect"; -import { legacyProjectStatePaths } from "./legacy-project-state.paths.ts"; +import { legacyProjectStatePaths } from "../shared/config/legacy-project-state.paths.ts"; import type { ContainerIdName } from "./docker-lifecycle.ts"; diff --git a/apps/cli/src/command-internal/status-errors.ts b/apps/cli/src/command-internal/status-errors.ts index 313c7fb05e..5f5e8e382f 100644 --- a/apps/cli/src/command-internal/status-errors.ts +++ b/apps/cli/src/command-internal/status-errors.ts @@ -18,7 +18,7 @@ export class StatusWorkdirError extends Data.TaggedError("StatusWorkdirError")<{ } } -/** `loadCliConfig` rejected `supabase/config.toml` (malformed TOML/JSON). */ +/** The config load rejected `supabase/config.toml` (malformed TOML/JSON). */ export class StatusConfigLoadError extends Data.TaggedError("StatusConfigLoadError")<{ readonly message: string; }> { diff --git a/apps/cli/src/command-internal/status-pretty.ts b/apps/cli/src/command-internal/status-pretty.ts index 62ab0232d5..ec400a0d9a 100644 --- a/apps/cli/src/command-internal/status-pretty.ts +++ b/apps/cli/src/command-internal/status-pretty.ts @@ -78,7 +78,7 @@ export function statusGroups( /** * Display width for this command's inputs: URLs/keys/labels are always plain ASCII, so every - * rune is width 1. The fixed group-title emoji are the only non-ASCII runes ever rendered, + * character is width 1. The fixed group-title emoji are the only non-ASCII characters ever rendered, * and their widths are hardcoded in {@link HEADER_DISPLAY_WIDTH} instead of computed * generically. */ diff --git a/apps/cli/src/command-internal/status-pretty.unit.test.ts b/apps/cli/src/command-internal/status-pretty.unit.test.ts index c98a8cb76b..f410a98633 100644 --- a/apps/cli/src/command-internal/status-pretty.unit.test.ts +++ b/apps/cli/src/command-internal/status-pretty.unit.test.ts @@ -45,7 +45,7 @@ const FULL_VALUES: Record = { }; describe("renderStatusPretty", () => { - it("matches the Go rounded-table fixture for a fully running stack", () => { + it("matches the rounded-table fixture for a fully running stack", () => { const out = stripAnsi(renderStatusPretty(FULL_VALUES, NAMES)); const expected = [ @@ -94,7 +94,7 @@ describe("renderStatusPretty", () => { }); // All other groups are empty in this fixture, so only the Database box should appear. - it("matches the Go rounded-table fixture for a single-row group", () => { + it("matches the rounded-table fixture for a single-row group", () => { const out = stripAnsi( renderStatusPretty({ DB_URL: FULL_VALUES.DB_URL ?? "" }, { ...NAMES, dbUrl: "DB_URL" }), ); @@ -112,7 +112,7 @@ describe("renderStatusPretty", () => { }); // All other groups are empty in this fixture; only Project URL is present in APIs. - it("matches the Go rounded-table fixture for a partial APIs group", () => { + it("matches the rounded-table fixture for a partial APIs group", () => { const out = stripAnsi(renderStatusPretty({ API_URL: "http://127.0.0.1:54321" }, NAMES)); const expectedTable = [ diff --git a/apps/cli/src/command-internal/status-values.ts b/apps/cli/src/command-internal/status-values.ts index 782624ab35..530e8146a9 100644 --- a/apps/cli/src/command-internal/status-values.ts +++ b/apps/cli/src/command-internal/status-values.ts @@ -3,16 +3,12 @@ import { Effect } from "effect"; import { dockerfileServiceImageRaw } from "../shared/services/dockerfile-images.ts"; import { serviceContainerIds } from "./docker-ids.ts"; -import { - envOverrideBool, - resolveLocalConfigValues, - type LocalConfigValues, -} from "./local-config-values.ts"; +import { resolveLocalConfigValues, type LocalConfigValues } from "./local-config-values.ts"; /** * A status output field: the dotted key `--override-name =` matches against * (`fieldKey`), and its default output env-var name (`defaultName`). Deprecated fields - * (`inbucket`, `jwt_secret`, `anon_key`, `service_role_key`) are still emitted. + * (`jwt_secret`, `anon_key`, `service_role_key`) are still emitted. */ export interface StatusField { readonly fieldKey: string; @@ -33,7 +29,6 @@ const FUNCTIONS_URL: StatusField = { }; const DB_URL: StatusField = { fieldKey: "db.url", defaultName: "DB_URL" }; const STUDIO_URL: StatusField = { fieldKey: "studio.url", defaultName: "STUDIO_URL" }; -const INBUCKET_URL: StatusField = { fieldKey: "inbucket.url", defaultName: "INBUCKET_URL" }; const MAILPIT_URL: StatusField = { fieldKey: "mailpit.url", defaultName: "MAILPIT_URL" }; const PUBLISHABLE_KEY: StatusField = { fieldKey: "auth.publishable_key", @@ -59,7 +54,7 @@ const STORAGE_S3_REGION: StatusField = { defaultName: "S3_PROTOCOL_REGION", }; -/** All 18 fields, in declaration order. */ +/** All 17 fields, in declaration order. */ export const STATUS_FIELDS: ReadonlyArray = [ API_URL, REST_URL, @@ -69,7 +64,6 @@ export const STATUS_FIELDS: ReadonlyArray = [ FUNCTIONS_URL, DB_URL, STUDIO_URL, - INBUCKET_URL, MAILPIT_URL, PUBLISHABLE_KEY, SECRET_KEY, @@ -231,17 +225,12 @@ export interface StatusLocalState { /** * Resolves local config values (URLs, keys — can fail, see {@link resolveLocalConfigValues}) * and the per-service `.enabled` gates, with no reference to `excluded`/`containerIds` — see - * {@link gateStatusState} for the Docker-dependent half this composes with. Each `.enabled` - * gate is read through {@link envOverrideBool}, not the raw decoded `config.
.enabled`, - * so an env-overridden stack's running services match what `status` reports. + * {@link gateStatusState} for the Docker-dependent half this composes with. `config` must be + * the resolved config's materialized config so env overrides are already applied to every gate. * - * Fails with {@link InvalidJwtSecretError} when `auth.jwt_secret` is set but too short. - * Fails with {@link InvalidPortEnvOverrideError} when a `SUPABASE_*_PORT` env/dotenv - * override doesn't parse as a valid port. - * Fails with {@link InvalidBoolEnvOverrideError} when a `SUPABASE_*_ENABLED` env/dotenv - * override doesn't parse as a valid bool. - * Fails when `auth.signing_keys_path` is set but the file is missing, malformed, or its - * first key is unsupported — see {@link generateAsymmetricGoJwt}. + * @throws {InvalidJwtSecretError} when `auth.jwt_secret` is set but too short. + * @throws when `auth.signing_keys_path` is set but the file is missing, malformed, or its + * first key is unsupported — see {@link generateAsymmetricLocalJwt}. */ export const resolveStatusLocalState = Effect.fnUntraced(function* ( config: CliConfig, @@ -254,58 +243,22 @@ export const resolveStatusLocalState = Effect.fnUntraced(function* ( * An already-resolved {@link resolveLocalConfigValues} result to reuse instead of * re-deriving one. Callers that resolved `local` earlier in the same process (e.g. * `start`'s success-path status print) must pass it here: a second call re-mints a - * time-dependent asymmetric JWT (`auth.signing_keys_path` + {@link generateAsymmetricGoJwt}'s + * time-dependent asymmetric JWT (`auth.signing_keys_path` + {@link generateAsymmetricLocalJwt}'s * `exp` claim) with a different signature than the one baked into the already-running * containers. */ precomputedLocal?: LocalConfigValues, ) { const local = - precomputedLocal ?? - (yield* resolveLocalConfigValues(config, hostname, workdir, projectEnvValues, document)); + precomputedLocal ?? (yield* resolveLocalConfigValues(config, hostname, workdir, document)); - const apiEnabled = yield* envOverrideBool( - "SUPABASE_API_ENABLED", - config.api.enabled, - "api.enabled", - projectEnvValues, - ); - const studioSectionEnabled = yield* envOverrideBool( - "SUPABASE_STUDIO_ENABLED", - config.studio.enabled, - "studio.enabled", - projectEnvValues, - ); - const authSectionEnabled = yield* envOverrideBool( - "SUPABASE_AUTH_ENABLED", - config.auth.enabled, - "auth.enabled", - projectEnvValues, - ); - const inbucketSectionEnabled = yield* envOverrideBool( - "SUPABASE_LOCAL_SMTP_ENABLED", - config.local_smtp.enabled, - "local_smtp.enabled", - projectEnvValues, - ); - const storageSectionEnabled = yield* envOverrideBool( - "SUPABASE_STORAGE_ENABLED", - config.storage.enabled, - "storage.enabled", - projectEnvValues, - ); - const edgeRuntimeEnabled = yield* envOverrideBool( - "SUPABASE_EDGE_RUNTIME_ENABLED", - config.edge_runtime.enabled, - "edge_runtime.enabled", - projectEnvValues, - ); - const storageS3ProtocolEnabled = yield* envOverrideBool( - "SUPABASE_STORAGE_S3_PROTOCOL_ENABLED", - config.storage.s3_protocol.enabled, - "storage.s3_protocol.enabled", - projectEnvValues, - ); + const apiEnabled = config.api.enabled; + const studioSectionEnabled = config.studio.enabled; + const authSectionEnabled = config.auth.enabled; + const inbucketSectionEnabled = config.local_smtp.enabled; + const storageSectionEnabled = config.storage.enabled; + const edgeRuntimeEnabled = config.edge_runtime.enabled; + const storageS3ProtocolEnabled = config.storage.s3_protocol.enabled; return { config, @@ -412,7 +365,6 @@ export function statusValuesFromState( } if (inbucketEnabled) { values[names.mailpitUrl] = local.mailpitUrl; - values[overrides.get(INBUCKET_URL.fieldKey) ?? INBUCKET_URL.defaultName] = local.mailpitUrl; } if (storageEnabled && storageS3ProtocolEnabled) { values[names.storageS3Url] = local.storageS3Url; @@ -424,16 +376,6 @@ export function statusValuesFromState( return { values, names, local }; } -/** The stderr warning for the deprecated `INBUCKET_URL` field, when {@link statusValuesFromState} emits it. */ -export function deprecatedStatusFieldWarning( - state: StatusState, - overrides: ReadonlyMap, -): string | undefined { - if (!state.inbucketEnabled) return undefined; - const nameFor = (field: StatusField) => overrides.get(field.fieldKey) ?? field.defaultName; - return `WARN: ${nameFor(INBUCKET_URL)} is deprecated and will be removed in v3, use ${nameFor(MAILPIT_URL)} instead.\n`; -} - /** * Convenience wrapper combining {@link resolveStatusLocalState}, {@link gateStatusState}, and * {@link statusValuesFromState} in one call — used by tests needing only a single override diff --git a/apps/cli/src/command-internal/status-values.unit.test.ts b/apps/cli/src/command-internal/status-values.unit.test.ts index f09b8ca268..e4cb1956e3 100644 --- a/apps/cli/src/command-internal/status-values.unit.test.ts +++ b/apps/cli/src/command-internal/status-values.unit.test.ts @@ -316,24 +316,23 @@ describe("statusValues", () => { }); describe("inbucket/mailpit gating", () => { - it("includes MAILPIT_URL and the deprecated INBUCKET_URL alias when local_smtp.enabled", () => { + it("emits MAILPIT_URL and no INBUCKET_URL when local_smtp.enabled", () => { const { values } = run( statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR), ); expect(values.MAILPIT_URL).toBeDefined(); - expect(values.INBUCKET_URL).toBe(values.MAILPIT_URL); + expect(values.INBUCKET_URL).toBeUndefined(); }); - it("omits MAILPIT_URL/INBUCKET_URL when local_smtp.enabled is false", () => { + it("omits MAILPIT_URL when local_smtp.enabled is false", () => { const config = baseConfig({ local_smtp: { enabled: false } }); const { values } = run( statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR), ); expect(values.MAILPIT_URL).toBeUndefined(); - expect(values.INBUCKET_URL).toBeUndefined(); }); - it("omits MAILPIT_URL/INBUCKET_URL when the inbucket container id is excluded", () => { + it("omits MAILPIT_URL when the inbucket container id is excluded", () => { const { values } = run( statusValues( baseConfig(), @@ -347,7 +346,7 @@ describe("statusValues", () => { expect(values.MAILPIT_URL).toBeUndefined(); }); - it("omits MAILPIT_URL/INBUCKET_URL when the mailpit image short name is excluded", () => { + it("omits MAILPIT_URL when the mailpit image short name is excluded", () => { const { values } = run( statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, ["mailpit"], NO_OVERRIDES, WORKDIR), ); @@ -407,112 +406,6 @@ describe("statusValues", () => { }); }); - describe("SUPABASE_*_ENABLED env overrides", () => { - // `resolveStatusLocalState` must read the post-env-override value for every `.enabled` - // gate, not the raw decoded `config.
.enabled`. - - it("includes API_URL/REST_URL when SUPABASE_API_ENABLED overrides a disabled api.enabled", () => { - const config = baseConfig({ api: { enabled: false } }); - const { values } = run( - statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR, { - SUPABASE_API_ENABLED: "true", - }), - ); - expect(values.API_URL).toBeDefined(); - expect(values.REST_URL).toBeDefined(); - }); - - it("omits REST_URL when SUPABASE_API_ENABLED=false overrides an enabled api.enabled", () => { - const { values } = run( - statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR, { - SUPABASE_API_ENABLED: "false", - }), - ); - expect(values.REST_URL).toBeUndefined(); - expect(values.API_URL).toBeDefined(); - }); - - it("includes STUDIO_URL when SUPABASE_STUDIO_ENABLED overrides a disabled studio.enabled", () => { - const config = baseConfig({ studio: { enabled: false } }); - const { values } = run( - statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR, { - SUPABASE_STUDIO_ENABLED: "true", - }), - ); - expect(values.STUDIO_URL).toBeDefined(); - }); - - it("includes the 5 auth fields when SUPABASE_AUTH_ENABLED overrides a disabled auth.enabled", () => { - const config = baseConfig({ auth: { enabled: false } }); - const { values } = run( - statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR, { - SUPABASE_AUTH_ENABLED: "true", - }), - ); - expect(values.PUBLISHABLE_KEY).toBeDefined(); - expect(values.ANON_KEY).toBeDefined(); - expect(values.SERVICE_ROLE_KEY).toBeDefined(); - }); - - it("omits the 5 auth fields when SUPABASE_AUTH_ENABLED=false overrides an enabled auth.enabled", () => { - const { values } = run( - statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR, { - SUPABASE_AUTH_ENABLED: "false", - }), - ); - expect(values.PUBLISHABLE_KEY).toBeUndefined(); - }); - - it("includes MAILPIT_URL when SUPABASE_LOCAL_SMTP_ENABLED overrides a disabled local_smtp.enabled", () => { - const config = baseConfig({ local_smtp: { enabled: false } }); - const { values } = run( - statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR, { - SUPABASE_LOCAL_SMTP_ENABLED: "true", - }), - ); - expect(values.MAILPIT_URL).toBeDefined(); - }); - - it("includes storage S3 fields when SUPABASE_STORAGE_ENABLED overrides a disabled storage.enabled", () => { - const config = baseConfig({ storage: { enabled: false } }); - const { values } = run( - statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR, { - SUPABASE_STORAGE_ENABLED: "true", - }), - ); - expect(values.STORAGE_S3_URL).toBeDefined(); - }); - - it("includes FUNCTIONS_URL when SUPABASE_EDGE_RUNTIME_ENABLED overrides a disabled edge_runtime.enabled", () => { - const config = baseConfig({ edge_runtime: { enabled: false } }); - const { values } = run( - statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR, { - SUPABASE_EDGE_RUNTIME_ENABLED: "true", - }), - ); - expect(values.FUNCTIONS_URL).toBeDefined(); - }); - - it("includes storage S3 fields when SUPABASE_STORAGE_S3_PROTOCOL_ENABLED overrides a disabled s3_protocol.enabled", () => { - const config = baseConfig({ storage: { s3_protocol: { enabled: false } } }); - const { values } = run( - statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR, { - SUPABASE_STORAGE_S3_PROTOCOL_ENABLED: "true", - }), - ); - expect(values.STORAGE_S3_URL).toBeDefined(); - }); - - it("omits storage S3 fields when SUPABASE_STORAGE_S3_PROTOCOL_ENABLED=false overrides an enabled s3_protocol.enabled", () => { - const { values } = run( - statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR, { - SUPABASE_STORAGE_S3_PROTOCOL_ENABLED: "false", - }), - ); - expect(values.STORAGE_S3_URL).toBeUndefined(); - }); - }); - describe("--override-name remapping", () => { it("remaps a field's output KEY while leaving the value unchanged", () => { const overrides = new Map([["api.url", "NEXT_PUBLIC_SUPABASE_URL"]]); @@ -561,16 +454,6 @@ describe("statusValues", () => { expect(values.ANON_KEY).toBeUndefined(); expect(values.SERVICE_ROLE_KEY).toBeUndefined(); }); - - it("remaps the deprecated inbucket.url key independently of mailpit.url", () => { - const overrides = new Map([["inbucket.url", "CUSTOM_INBUCKET_URL"]]); - const { values } = run( - statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, NONE, overrides, WORKDIR), - ); - expect(values.CUSTOM_INBUCKET_URL).toBeDefined(); - expect(values.MAILPIT_URL).toBeDefined(); - expect(values.INBUCKET_URL).toBeUndefined(); - }); }); it("combines stopped-service exclusions with --exclude flag exclusions", () => { diff --git a/apps/cli/src/command-internal/storage-bucket-config.unit.test.ts b/apps/cli/src/command-internal/storage-bucket-config.unit.test.ts index 042e29ec97..c33d935f48 100644 --- a/apps/cli/src/command-internal/storage-bucket-config.unit.test.ts +++ b/apps/cli/src/command-internal/storage-bucket-config.unit.test.ts @@ -15,7 +15,7 @@ describe("parseFileSizeLimit", () => { expect(() => parseFileSizeLimit("not-a-size")).toThrow(); }); - it("accepts Go-valid numeral forms (strconv.ParseFloat parity)", () => { + it("accepts valid numeral forms", () => { expect(parseFileSizeLimit(".5MiB")).toBe(Math.trunc(0.5 * 1024 * 1024)); expect(parseFileSizeLimit("1.MiB")).toBe(1024 * 1024); expect(parseFileSizeLimit("1e6")).toBe(1_000_000); @@ -23,7 +23,7 @@ describe("parseFileSizeLimit", () => { expect(parseFileSizeLimit("1_0MiB")).toBe(10 * 1024 * 1024); }); - it("rejects badly-placed underscores (Go literal rule)", () => { + it("rejects badly-placed underscores", () => { expect(() => parseFileSizeLimit("_1000MiB")).toThrow("invalid size"); expect(() => parseFileSizeLimit("1__0MiB")).toThrow("invalid size"); }); @@ -34,7 +34,7 @@ describe("parseFileSizeLimit", () => { expect(() => parseFileSizeLimit("-5MiB")).toThrow("invalid size"); }); - it("rejects an overflowing numeral (Go ParseFloat range error)", () => { + it("rejects an overflowing numeral", () => { expect(() => parseFileSizeLimit("1e309")).toThrow("invalid size"); }); }); diff --git a/apps/cli/src/command-internal/storage-content-type.unit.test.ts b/apps/cli/src/command-internal/storage-content-type.unit.test.ts index dc249caa1c..fac59a79f7 100644 --- a/apps/cli/src/command-internal/storage-content-type.unit.test.ts +++ b/apps/cli/src/command-internal/storage-content-type.unit.test.ts @@ -68,13 +68,11 @@ layer(BunPath.layer)("contentTypeForUpload", (it) => { }); layer(BunPath.layer)("refineUploadContentType", (it) => { - it.effect( - "refines an explicit text/plain content-type by extension (Go refines the flag too)", - () => - Effect.gen(function* () { - const path = yield* Path.Path; - expect(refineUploadContentType(path, "text/plain", "/x/a.json")).toBe("application/json"); - }), + it.effect("refines an explicit text/plain content-type by extension", () => + Effect.gen(function* () { + const path = yield* Path.Path; + expect(refineUploadContentType(path, "text/plain", "/x/a.json")).toBe("application/json"); + }), ); it.effect("leaves a non-text content-type untouched", () => diff --git a/apps/cli/src/command-internal/storage-credentials.integration.test.ts b/apps/cli/src/command-internal/storage-credentials.integration.test.ts index a7b5d4e044..4a8448d787 100644 --- a/apps/cli/src/command-internal/storage-credentials.integration.test.ts +++ b/apps/cli/src/command-internal/storage-credentials.integration.test.ts @@ -7,6 +7,7 @@ import { mockCommandPlatformApi, mockCommandSettings, } from "../../tests/helpers/command-mocks.ts"; +import { cliConfigValuesTestLayer } from "../../tests/helpers/config-values-layer.ts"; import { mockRuntimeInfo } from "../../tests/helpers/mocks.ts"; import { assertBlockDrift } from "../../tests/helpers/permission-drift.ts"; import { unusedStackServices } from "../../tests/helpers/unused-stack.ts"; @@ -16,18 +17,18 @@ const apiKeys = [ { name: "anon", api_key: "anon-key", type: "legacy" }, { name: "service_role", api_key: "service-role-key", type: "legacy" }, ]; -const config = { api: { enabled: true, port: 54321, tls: { enabled: false } }, auth: {} }; describe("resolveStorageCredentials for a remote project", () => { const resolve = ( api: ReturnType, env: Record = {}, ) => - resolveStorageCredentials({ projectRef: VALID_REF, config }).pipe( + resolveStorageCredentials({ projectRef: VALID_REF }).pipe( Effect.provide( Layer.mergeAll( ConfigProvider.layer(ConfigProvider.fromEnvRecord(env)), api.factoryLayer, + cliConfigValuesTestLayer, mockCommandSettings({ workdir: "/unused" }), mockRuntimeInfo(), unusedStackServices, diff --git a/apps/cli/src/command-internal/storage-credentials.ts b/apps/cli/src/command-internal/storage-credentials.ts index 5c25c886fb..29fb839fcf 100644 --- a/apps/cli/src/command-internal/storage-credentials.ts +++ b/apps/cli/src/command-internal/storage-credentials.ts @@ -1,21 +1,18 @@ -import { Config, Effect, FileSystem, Option, Path } from "effect"; +import { Effect, FileSystem, Option, Path, Result } from "effect"; import { FetchHttpClient } from "effect/http"; import { CommandPlatformApiFactory } from "../auth/command-platform-api-factory.service.ts"; import { CommandSettings } from "../config/command-settings.service.ts"; +import { pickCliEnvName } from "../config/cli-config-key.ts"; +import { CliEnvNames } from "../config/cli-config-keys.ts"; +import { readShellEnvironment } from "../shared/config/cli-config-env.ts"; import { resolveApiExternalUrl } from "./api-url.ts"; import { validateApiPort, validateApiTlsPresence } from "./config-validate.ts"; -import { loadProjectEnv } from "./db-config.toml-read.ts"; +import { describeConfigLoadFailure, loadResolvedConfigContext } from "./resolved-config-context.ts"; import { mapTenantApiKeysError } from "./get-tenant-api-keys.ts"; -import { generateGoJwt } from "./go-jwt.ts"; +import { generateLocalJwt } from "./local-jwt.ts"; import { getHostname } from "./hostname.ts"; -import { - decryptAuthSecret, - envOverride, - envOverrideBool, - envOverridePort, - resolveJwtSecret, -} from "./local-config-values.ts"; +import { resolveJwtSecret } from "./local-config-values.ts"; import { KONG_LOCAL_CA_CERT } from "./kong-local-ca-cert.ts"; import { extractServiceKeys } from "./tenant-keys.ts"; import { @@ -78,28 +75,27 @@ export interface StorageCredentials { export const resolveStorageCredentials = Effect.fnUntraced(function* (opts: { readonly projectRef: string; - readonly config: StorageConfigView; - /** - * Already-resolved project env map for the `SUPABASE_API_*`/`SUPABASE_AUTH_*` - * overrides, when the caller has one in scope. When omitted, this loads the - * project dotenv itself. - */ - readonly projectEnvValues?: Readonly>; }) { const cliSettings = yield* CommandSettings; if (opts.projectRef !== "") { const baseUrl = `https://${opts.projectRef}.${cliSettings.projectHost}`; - const envKey = yield* Config.option(Config.String("SUPABASE_AUTH_SERVICE_ROLE_KEY")).pipe( - Effect.mapError( - () => - new StorageConfigError({ - message: "failed to resolve environment variable: SUPABASE_AUTH_SERVICE_ROLE_KEY", - }), - ), + const shell = yield* readShellEnvironment({ + names: [CliEnvNames.authServiceRoleKey.name], + }).pipe(Effect.mapError(toStorageConfigError)); + const envKey = yield* Result.match( + pickCliEnvName(CliEnvNames.authServiceRoleKey, { shell: shell.get }), + { + onFailure: (error) => Effect.fail(toStorageConfigError(error)), + onSuccess: Effect.succeed, + }, ); - if (Option.isSome(envKey) && envKey.value.length > 0) { - return { baseUrl, apiKey: envKey.value, localKongCa: undefined } satisfies StorageCredentials; + if (Option.isSome(envKey)) { + return { + baseUrl, + apiKey: envKey.value, + localKongCa: undefined, + } satisfies StorageCredentials; } // Resolved lazily so the local path never triggers auth. const api = yield* (yield* CommandPlatformApiFactory).make; @@ -130,19 +126,15 @@ export const resolveStorageCredentials = Effect.fnUntraced(function* (opts: { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const projectEnvValues = - opts.projectEnvValues ?? - (yield* loadProjectEnv(fs, path, cliSettings.workdir).pipe( - Effect.mapError((cause) => new StorageConfigError({ message: cause.message })), - )); - const api = yield* resolveLocalApiConfig(opts.config.api, projectEnvValues); + const { config, resolvedConfig } = yield* loadLocalStorageConfig(cliSettings.workdir); + const api = yield* resolveLocalApiConfig(config.api); const baseUrl = resolveApiExternalUrl( api, - yield* getHostname(projectEnvValues).pipe( + yield* getHostname(resolvedConfig).pipe( Effect.mapError((cause) => new StorageConfigError({ message: cause.message })), ), ); - const apiKey = yield* resolveLocalServiceRoleKey(opts.config.auth, projectEnvValues); + const apiKey = yield* resolveLocalServiceRoleKey(config.auth); // Validate the cert/key pairing only when the API and TLS are both enabled; // inject a CA whenever the resolved URL is https. @@ -164,8 +156,7 @@ export const resolveStorageCredentials = Effect.fnUntraced(function* (opts: { }); /** - * Converts a config-load validation error (from `envOverride*`, - * `decryptAuthSecret`, `resolveJwtSecret`, `validateApi*`) into a tagged + * Converts a thrown config-load validation error (from `resolveJwtSecret`, `validateApi*`) into a tagged * `StorageConfigError`, preserving the original message. */ const toStorageConfigError = (cause: unknown) => @@ -173,87 +164,50 @@ const toStorageConfigError = (cause: unknown) => message: cause instanceof Error ? cause.message : String(cause), }); -/** - * Folds `SUPABASE_API_*` overrides into `[api]` before deriving the gateway - * URL, so a stack started with an overridden port stays reachable here - * instead of falling back to the raw `config.toml` value. - */ -const resolveLocalApiConfig = Effect.fnUntraced(function* ( - api: StorageConfigView["api"], - projectEnvValues: Readonly>, -) { - const resolved = { - enabled: yield* envOverrideBool( - "SUPABASE_API_ENABLED", - api.enabled, - "api.enabled", - projectEnvValues, +const loadLocalStorageConfig = (workdir: string) => + loadResolvedConfigContext(workdir).pipe( + Effect.mapError( + (cause) => new StorageConfigError({ message: describeConfigLoadFailure(cause) }), ), - external_url: yield* envOverride( - "SUPABASE_API_EXTERNAL_URL", - api.external_url, - projectEnvValues, - ), - port: yield* envOverridePort("SUPABASE_API_PORT", api.port, "api.port", projectEnvValues), - tls: { - enabled: yield* envOverrideBool( - "SUPABASE_API_TLS_ENABLED", - api.tls.enabled, - "api.tls.enabled", - projectEnvValues, - ), - cert_path: yield* envOverride( - "SUPABASE_API_TLS_CERT_PATH", - api.tls.cert_path, - projectEnvValues, - ), - key_path: yield* envOverride("SUPABASE_API_TLS_KEY_PATH", api.tls.key_path, projectEnvValues), + ); + +/** The effective `[api]` view, with the port and TLS pairing checked, for deriving the gateway URL. */ +const resolveLocalApiConfig = (api: StorageConfigView["api"]) => + Effect.try({ + try: () => { + validateApiPort(api.enabled, api.port); + return api; }, - } satisfies StorageConfigView["api"]; - yield* Effect.try({ - try: () => validateApiPort(resolved.enabled, resolved.port), catch: toStorageConfigError, }); - return resolved; -}, Effect.mapError(toStorageConfigError)); /** * Resolves the service-role key for the local Storage gateway: - * - jwt secret: `SUPABASE_AUTH_JWT_SECRET` → `auth.jwt_secret` → - * `defaultJwtSecret`, rejected if shorter than 16 chars. - * - service-role key: `SUPABASE_AUTH_SERVICE_ROLE_KEY` → - * `auth.service_role_key` → signed from the resolved jwt secret. + * - jwt secret: `auth.jwt_secret` → `defaultJwtSecret`, rejected if shorter than 16 chars. + * - service-role key: `auth.service_role_key` → signed from the resolved jwt secret. * * An explicit `service_role_key = ""` is treated as unset and regenerated. */ -const resolveLocalServiceRoleKey = Effect.fnUntraced(function* ( - auth: StorageConfigView["auth"], - projectEnvValues: Readonly>, -) { - const jwtSecret = yield* decryptAuthSecret( - yield* envOverride("SUPABASE_AUTH_JWT_SECRET", auth.jwt_secret, projectEnvValues), - projectEnvValues, - ).pipe(Effect.flatMap(resolveJwtSecret), Effect.mapError(toStorageConfigError)); - const configuredKey = yield* decryptAuthSecret( - yield* envOverride("SUPABASE_AUTH_SERVICE_ROLE_KEY", auth.service_role_key, projectEnvValues), - projectEnvValues, - ).pipe(Effect.mapError(toStorageConfigError)); +const resolveLocalServiceRoleKey = Effect.fnUntraced(function* (auth: StorageConfigView["auth"]) { + const jwtSecret = yield* Effect.try({ + try: () => resolveJwtSecret(auth.jwt_secret), + catch: toStorageConfigError, + }); + const configuredKey = auth.service_role_key; return configuredKey !== undefined && configuredKey.length > 0 ? configuredKey - : generateGoJwt(jwtSecret, "service_role"); + : generateLocalJwt(jwtSecret, "service_role"); }); /** - * Runs the local config-load validations (API overrides, auth secret - * decryption, TLS presence) without building credentials, for `seed - * buckets`'s empty-config short-circuit. + * Runs the local config-load validations (API port, auth secrets, TLS presence) without building + * credentials, for `seed buckets`'s empty-config short-circuit. */ -export const validateLocalStorageConfig = Effect.fnUntraced(function* ( - config: StorageConfigView, - projectEnvValues: Readonly>, -) { - const api = yield* resolveLocalApiConfig(config.api, projectEnvValues); - yield* resolveLocalServiceRoleKey(config.auth, projectEnvValues); +export const validateLocalStorageConfig = Effect.fnUntraced(function* () { + const cliSettings = yield* CommandSettings; + const { config } = yield* loadLocalStorageConfig(cliSettings.workdir); + const api = yield* resolveLocalApiConfig(config.api); + yield* resolveLocalServiceRoleKey(config.auth); if (api.enabled && api.tls.enabled) { yield* Effect.try({ try: () => validateApiTlsPresence(api.tls.cert_path, api.tls.key_path), diff --git a/apps/cli/src/command-internal/storage-gateway.ts b/apps/cli/src/command-internal/storage-gateway.ts index 3e20d9cca9..6190f17106 100644 --- a/apps/cli/src/command-internal/storage-gateway.ts +++ b/apps/cli/src/command-internal/storage-gateway.ts @@ -4,9 +4,11 @@ import * as HttpClientError from "effect/http/HttpClientError"; import * as HttpClientRequest from "effect/http/HttpClientRequest"; import * as HttpClientResponse from "effect/http/HttpClientResponse"; +import { announceTarget } from "../shared/output/command-target.service.ts"; import { kongAuthHeaders } from "./kong-auth.ts"; import { StorageGatewayNetworkError, StorageGatewayStatusError } from "./storage-gateway.errors.ts"; -import { goPathSplit } from "./storage-url.ts"; +import { splitDirAndFile } from "./storage-url.ts"; +import { renderLocalTarget } from "./target/target-identity.ts"; /** * Client for the Supabase Storage service gateway (Kong). See @@ -149,6 +151,15 @@ function localGatewayHint(port: string): string { ); } +/** Announces the local Storage API endpoint as the run's target. */ +export const announceLocalStorageTarget = (baseUrl: string) => { + const { hostname, port, protocol } = new URL(baseUrl); + const defaultPort = protocol === "https:" ? 443 : 80; + return announceTarget( + renderLocalTarget(hostname.replace(/^\[|\]$/g, ""), port === "" ? defaultPort : Number(port)), + ); +}; + /** * Whether a transport failure is a plain connection-refused. The * port-conflict hint is suppressed for these, since a refused connection @@ -420,7 +431,7 @@ export const makeStorageGateway = Effect.fnUntraced(function* (opts: { Effect.flatMap((body) => decodeFieldResponse(body, "message")), ), listObjects: (bucket, prefix, page) => { - const [dir, name] = goPathSplit(prefix); + const [dir, name] = splitDirAndFile(prefix); const query: Record = { prefix: dir }; if (name.length > 0) query["search"] = name; query["limit"] = PAGE_LIMIT; diff --git a/apps/cli/src/command-internal/storage-gateway.unit.test.ts b/apps/cli/src/command-internal/storage-gateway.unit.test.ts index ae4aa51155..0ae48a9be2 100644 --- a/apps/cli/src/command-internal/storage-gateway.unit.test.ts +++ b/apps/cli/src/command-internal/storage-gateway.unit.test.ts @@ -19,7 +19,7 @@ import { StorageGatewayNetworkError, StorageGatewayStatusError } from "./storage import { bucketBody, makeStorageGateway } from "./storage-gateway.ts"; describe("bucketBody", () => { - it("omits public when undefined (Go *bool nil / omitempty)", () => { + it("omits public when undefined", () => { expect(bucketBody({ public: undefined, fileSizeLimit: 0, allowedMimeTypes: [] })).toEqual({}); }); diff --git a/apps/cli/src/command-internal/storage-permissions.ts b/apps/cli/src/command-internal/storage-permissions.ts new file mode 100644 index 0000000000..0391e20b4c --- /dev/null +++ b/apps/cli/src/command-internal/storage-permissions.ts @@ -0,0 +1,12 @@ +import { linkedProjectCache } from "../telemetry/linked-project-cache.layer.ts"; +import { compose, withCondition } from "./command-permissions/model.ts"; +import { storageCredentials } from "./storage-credentials.ts"; +import { branchResolution } from "./target/branch-resolution.ts"; + +/** The Management API calls a Storage-API command makes on the linked project. */ +export const storagePermissions = (noApiEffectFlags: ReadonlyArray) => + compose( + { operations: [], noApiEffectFlags }, + withCondition(compose(storageCredentials, linkedProjectCache), { dbTarget: ["linked"] }), + branchResolution, + ); diff --git a/apps/cli/src/command-internal/storage-runtime.layer.ts b/apps/cli/src/command-internal/storage-runtime.layer.ts index c21642f664..46518fbfdf 100644 --- a/apps/cli/src/command-internal/storage-runtime.layer.ts +++ b/apps/cli/src/command-internal/storage-runtime.layer.ts @@ -6,8 +6,8 @@ import { commandPlatformApiFactoryLayer } from "../auth/command-platform-api-fac import { CommandPlatformApiFactory } from "../auth/command-platform-api-factory.service.ts"; import { commandSettingsLayer } from "../config/command-settings.layer.ts"; import { CommandSettings } from "../config/command-settings.service.ts"; -import { remoteTargetLayer } from "../config/remote-target.layer.ts"; -import { RemoteTarget } from "../config/remote-target.service.ts"; +import { projectRefLayer } from "../config/project-ref.layer.ts"; +import { ProjectRefResolver } from "../config/project-ref.service.ts"; import { debugLoggerLayer } from "./debug-logger.layer.ts"; import { IdentityStitch, identityStitchLayer } from "./identity-stitch.ts"; import { httpClientLayer } from "../auth/http-debug.layer.ts"; @@ -22,7 +22,7 @@ import { StackApi, stackApiLayer } from "./stack-api.ts"; /** * Runtime layer for the commands that talk to the Storage gateway directly: * `seed buckets` and `storage ls/cp/mv/rm`. The Management API client is lazy, - * so the local path (no `--linked`) never resolves a token or requires login. + * so the local default (no `--remote`) never resolves a token or requires login. * * `HttpClient` is exposed at the top level because the Storage gateway needs * it directly, not through the typed Management API client. @@ -35,7 +35,7 @@ export function storageGatewayRuntimeLayer(subcommand: ReadonlyArray) { Layer.provide(debugLoggerLayer), ); // Token resolution is deferred until `factory.make` is first called, on the - // `--linked` branch. + // hosted-target branch. const platformApiFactory = commandPlatformApiFactoryLayer.pipe( Layer.provide(credentials), Layer.provide(cliSettings), @@ -47,7 +47,7 @@ export function storageGatewayRuntimeLayer(subcommand: ReadonlyArray) { cliSettings, platformApiFactory, httpClient, - remoteTargetLayer.pipe(Layer.provide(platformApiFactory), Layer.provide(cliSettings)), + projectRefLayer.pipe(Layer.provide(platformApiFactory), Layer.provide(cliSettings)), linkedProjectCacheLayer.pipe( Layer.provide(credentials), Layer.provide(cliSettings), @@ -74,7 +74,7 @@ export function storageGatewayRuntimeLayer(subcommand: ReadonlyArray) { type StorageGatewayServices = | CommandPlatformApiFactory | CommandSettings - | RemoteTarget + | ProjectRefResolver | LinkedProjectCache | TelemetryState | IdentityStitch diff --git a/apps/cli/src/command-internal/storage-url.ts b/apps/cli/src/command-internal/storage-url.ts index be2c7ddbbd..b77a3bfde4 100644 --- a/apps/cli/src/command-internal/storage-url.ts +++ b/apps/cli/src/command-internal/storage-url.ts @@ -8,8 +8,8 @@ import { } from "../shared/telemetry/error-actionability.ts"; /** - * Storage URL parsing, matching Go's `net/url.Parse` semantics for the `ss://` - * scheme. `goUrlParse` implements `net/url.Parse` for the fields storage + * Storage URL parsing for the `ss://` + * scheme. `parseUrl` parses the fields storage * commands need (scheme, host, path); `parseStorageUrl` additionally requires * scheme `ss` (case-insensitive), a non-empty path, and no host. * @@ -22,10 +22,11 @@ export const STORAGE_SCHEME = "ss"; const STORAGE_INVALID_URL_MESSAGE = "URL must match pattern ss:///bucket/[prefix]"; /** - * Thrown when `goUrlParse` fails: `parse "": `. Callers wrap + * Thrown when `parseUrl` fails: `parse "": `. Callers wrap * `.message` in their own `failed to parse … url: ` text. */ -export class GoUrlParseError extends Data.Error<{ readonly message: string }> { +export class UrlParseError extends Data.Error<{ readonly message: string }> { + // The fingerprint and `name` are the stable telemetry/output identity of this error. static readonly [ErrorActionabilityFingerprintId] = "GoUrlParseError"; override readonly name = "GoUrlParseError"; constructor(rawURL: string, inner: string) { @@ -39,7 +40,7 @@ export class GoUrlParseError extends Data.Error<{ readonly message: string }> { /** * Thrown when a URL parses but does not match the `ss:///bucket/[prefix]` - * pattern. Distinct from `GoUrlParseError` so handlers can map it to a + * pattern. Distinct from `UrlParseError` so handlers can map it to a * separate tagged error. */ export class StorageUrlPatternError extends Data.Error<{ readonly message: string }> { @@ -54,7 +55,7 @@ export class StorageUrlPatternError extends Data.Error<{ readonly message: strin } } -export interface GoUrl { +export interface ParsedUrl { /** Lowercased scheme; `""` when none. */ readonly scheme: string; /** Authority host (after `//`); `""` when absent. */ @@ -168,12 +169,12 @@ function isValidOptionalPort(port: string): boolean { /** * Validates IP-literal (`[...]`) bracket/port syntax and `host[:port]` - * port-digit checks, matching Go's host validation for realistic inputs but + * port-digit checks, for realistic inputs but * skipping bracketed-literal IP-address checks and IPv6 zone-id decoding. * http/https treat the first colon as the port separator; other schemes use * the last. */ -function validateGoUrlHost(scheme: string, host: string): void { +function validateUrlHost(scheme: string, host: string): void { if (host.length === 0) return; const openBracketIdx = host.indexOf("["); if (openBracketIdx > 0) { @@ -201,16 +202,16 @@ function validateGoUrlHost(scheme: string, host: string): void { } /** - * Parses a URL for `scheme`/`host`/`path`, matching Go's `net/url.Parse`. - * Throws `GoUrlParseError` on parse failure. Query and fragment are stripped, + * Parses a URL for `scheme`/`host`/`path`. + * Throws `UrlParseError` on parse failure. Query and fragment are stripped, * though storage URLs never use them. */ -export function goUrlParse(rawURL: string): GoUrl { +export function parseUrl(rawURL: string): ParsedUrl { const hashIdx = rawURL.indexOf("#"); const u = hashIdx === -1 ? rawURL : rawURL.slice(0, hashIdx); if (containsCtlByte(u)) { - throw new GoUrlParseError(u, "net/url: invalid control character in URL"); + throw new UrlParseError(u, "net/url: invalid control character in URL"); } if (u === "*") { return { scheme: "", host: "", path: "*" }; @@ -223,7 +224,7 @@ export function goUrlParse(rawURL: string): GoUrl { scheme = parsed.scheme.toLowerCase(); rest = parsed.rest; } catch (cause) { - throw new GoUrlParseError(u, cause instanceof Error ? cause.message : String(cause)); + throw new UrlParseError(u, cause instanceof Error ? cause.message : String(cause)); } // Strip the query string. @@ -239,7 +240,7 @@ export function goUrlParse(rawURL: string): GoUrl { const slash = rest.indexOf("/"); const segment = slash === -1 ? rest : rest.slice(0, slash); if (segment.includes(":")) { - throw new GoUrlParseError(u, "first path segment in URL cannot contain colon"); + throw new UrlParseError(u, "first path segment in URL cannot contain colon"); } } @@ -253,9 +254,9 @@ export function goUrlParse(rawURL: string): GoUrl { rest = slash === -1 ? "" : afterSlashes.slice(slash); host = hostFromAuthority(authority); try { - validateGoUrlHost(scheme, host); + validateUrlHost(scheme, host); } catch (cause) { - throw new GoUrlParseError(u, cause instanceof Error ? cause.message : String(cause)); + throw new UrlParseError(u, cause instanceof Error ? cause.message : String(cause)); } } @@ -263,7 +264,7 @@ export function goUrlParse(rawURL: string): GoUrl { try { path = unescapePath(rest); } catch (cause) { - throw new GoUrlParseError(u, cause instanceof Error ? cause.message : String(cause)); + throw new UrlParseError(u, cause instanceof Error ? cause.message : String(cause)); } return { scheme, host, path }; } @@ -271,11 +272,11 @@ export function goUrlParse(rawURL: string): GoUrl { /** * Parses a storage URL, requiring scheme `ss` (case-insensitive), a * non-empty path, and no host, and returns the path. Throws - * `GoUrlParseError` on a parse failure, or `StorageUrlPatternError` when it + * `UrlParseError` on a parse failure, or `StorageUrlPatternError` when it * doesn't match `ss:///bucket/[prefix]`. */ export function parseStorageUrl(objectURL: string): string { - const parsed = goUrlParse(objectURL); + const parsed = parseUrl(objectURL); if (parsed.scheme !== STORAGE_SCHEME || parsed.path.length === 0 || parsed.host.length > 0) { throw new StorageUrlPatternError(); } @@ -301,10 +302,10 @@ export function splitBucketPrefix(objectPath: string): readonly [string, string] /** * Lowercased scheme for a raw URL; `""` when the URL has no scheme (a local - * path). Throws `GoUrlParseError` on a malformed URL (e.g. `:`). + * path). Throws `UrlParseError` on a malformed URL (e.g. `:`). */ export function detectScheme(rawURL: string): string { - return goUrlParse(rawURL).scheme; + return parseUrl(rawURL).scheme; } /** An object prefix is a directory when it is empty or ends with `/`. */ @@ -317,7 +318,7 @@ export function storageIsDir(objectPrefix: string): boolean { * trailing slash: `folder/name.png` → `["folder/", "name.png"]`; `dir` → * `["", "dir"]`; `tmp/` → `["tmp/", ""]`; `""` → `["", ""]`. */ -export function goPathSplit(p: string): readonly [string, string] { +export function splitDirAndFile(p: string): readonly [string, string] { const i = p.lastIndexOf("/"); return [p.slice(0, i + 1), p.slice(i + 1)]; } diff --git a/apps/cli/src/command-internal/storage-url.unit.test.ts b/apps/cli/src/command-internal/storage-url.unit.test.ts index 49ea25990f..fd33e21af2 100644 --- a/apps/cli/src/command-internal/storage-url.unit.test.ts +++ b/apps/cli/src/command-internal/storage-url.unit.test.ts @@ -2,10 +2,10 @@ import { describe, expect, it } from "vitest"; import { classifyCliErrorActionability } from "../shared/telemetry/error-actionability.ts"; import { - GoUrlParseError, + UrlParseError, StorageUrlPatternError, detectScheme, - goUrlParse, + parseUrl, parseStorageUrl, splitBucketPrefix, storageIsDir, @@ -32,7 +32,7 @@ describe("parseStorageUrl", () => { }); it("surfaces the url-parse error on a missing protocol scheme (:)", () => { - expect(() => parseStorageUrl(":")).toThrow(GoUrlParseError); + expect(() => parseStorageUrl(":")).toThrow(UrlParseError); expect(() => parseStorageUrl(":")).toThrow("missing protocol scheme"); }); @@ -41,7 +41,7 @@ describe("parseStorageUrl", () => { }); it("rejects a malformed percent-escape in the path", () => { - expect(() => parseStorageUrl("ss:///bucket/a%2")).toThrow(GoUrlParseError); + expect(() => parseStorageUrl("ss:///bucket/a%2")).toThrow(UrlParseError); expect(() => parseStorageUrl("ss:///bucket/a%2")).toThrow('invalid URL escape "%2"'); }); @@ -49,8 +49,8 @@ describe("parseStorageUrl", () => { expect(parseStorageUrl("ss:///bucket/a%20b")).toBe("/bucket/a b"); }); - it("decodes a multi-byte UTF-8 percent-escape as one rune (Go raw-byte path)", () => { - // %E4%B8%AD is the UTF-8 encoding of 中 — decoded as one rune, not three. + it("decodes a multi-byte UTF-8 percent-escape as one code point", () => { + // %E4%B8%AD is the UTF-8 encoding of 中 — decoded as one code point, not three. expect(parseStorageUrl("ss:///bucket/%E4%B8%AD.txt")).toBe("/bucket/中.txt"); }); }); @@ -86,50 +86,50 @@ describe("detectScheme", () => { }); // Host validation here also backs `studio.api_url` validation. -describe("goUrlParse (host validation)", () => { +describe("parseUrl (host validation)", () => { it("rejects an unterminated IPv6 literal", () => { - expect(() => goUrlParse("http://[::1")).toThrow(GoUrlParseError); - expect(() => goUrlParse("http://[::1")).toThrow(`parse "http://[::1": missing ']' in host`); + expect(() => parseUrl("http://[::1")).toThrow(UrlParseError); + expect(() => parseUrl("http://[::1")).toThrow(`parse "http://[::1": missing ']' in host`); }); it("accepts a bracketed IPv6 literal with no port", () => { - expect(goUrlParse("http://[::1]").host).toBe("[::1]"); + expect(parseUrl("http://[::1]").host).toBe("[::1]"); }); it("accepts a bracketed IPv6 literal with a valid port", () => { - expect(goUrlParse("http://[::1]:8080").host).toBe("[::1]:8080"); + expect(parseUrl("http://[::1]:8080").host).toBe("[::1]:8080"); }); it("rejects a bracketed IPv6 literal with a non-numeric port", () => { - expect(() => goUrlParse("http://[::1]:abc")).toThrow( + expect(() => parseUrl("http://[::1]:abc")).toThrow( `parse "http://[::1]:abc": invalid port ":abc" after host`, ); }); it("rejects a bracket that isn't the first character of the host", () => { - expect(() => goUrlParse("http://host[::1]")).toThrow( + expect(() => parseUrl("http://host[::1]")).toThrow( `parse "http://host[::1]": invalid IP-literal`, ); }); it("accepts a plain hostname with a numeric port", () => { - expect(goUrlParse("http://example.com:99999").host).toBe("example.com:99999"); + expect(parseUrl("http://example.com:99999").host).toBe("example.com:99999"); }); it("rejects more than one colon in an http(s) host (strict-colon default)", () => { - expect(() => goUrlParse("http://host:1:2")).toThrow( + expect(() => parseUrl("http://host:1:2")).toThrow( `parse "http://host:1:2": invalid port ":1:2" after host`, ); }); it("does not validate a host for a scheme with no authority (ss:///bucket)", () => { - expect(() => goUrlParse("ss:///bucket/x")).not.toThrow(); + expect(() => parseUrl("ss:///bucket/x")).not.toThrow(); }); }); describe("storage URL errors", () => { it("keep their names and telemetry fingerprints", () => { - const parseError = new GoUrlParseError(":", "missing protocol scheme"); + const parseError = new UrlParseError(":", "missing protocol scheme"); expect(String(parseError)).toBe('GoUrlParseError: parse ":": missing protocol scheme'); expect(classifyCliErrorActionability(parseError)).toMatchObject({ error_fingerprint: "error:GoUrlParseError", diff --git a/apps/cli/src/command-internal/string-slice-flag.ts b/apps/cli/src/command-internal/string-slice-flag.ts index 26b91b3e85..d56efae30a 100644 --- a/apps/cli/src/command-internal/string-slice-flag.ts +++ b/apps/cli/src/command-internal/string-slice-flag.ts @@ -6,8 +6,8 @@ import { } from "../shared/telemetry/error-actionability.ts"; /** - * Parses a pflag `StringSliceVar` flag: CSV-splits each occurrence, matching - * `encoding/csv`, and accumulates across repeats. A naive `.split(",")` + * Parses a repeatable string-slice flag: CSV-splits each occurrence + * and accumulates across repeats. A naive `.split(",")` * diverges on quoted/embedded commas (e.g. `'"a,b",c'`). * * Whitespace is not trimmed and empty fields are not dropped. @@ -66,8 +66,7 @@ export class StringSliceFlagParseError extends Data.Error<{ } /** - * Parses one CSV record from `val`, matching Go's `encoding/csv` reader as - * used by pflag's `readAsCSV`. Only the first record is read — an unquoted + * Parses one CSV record from `val`. Only the first record is read — an unquoted * newline ends it and the rest is dropped. Blank lines before the record are * skipped; blank-only input throws `EOF`. `\r\n` normalizes to `\n`, and * parse errors report 1-based line/byte-column positions. @@ -244,10 +243,10 @@ export function parseStringSliceFlag(rawValues: ReadonlyArray): Readonly } /** - * Builds a repeatable CSV-split flag matching pflag's `StringSliceVar` - * behavior, including its `invalid argument %q for %q flag: %v` diagnostic. + * Builds a repeatable CSV-split flag, including its `invalid argument "" for "" flag: ` + * diagnostic. * - * `options.alias` must be registered here, not piped on afterwards: pflag's + * `options.alias` must be registered here, not piped on afterwards: the * diagnostic frames both spellings (`-x, --exclude`), so the alias must be * present when the error message is built. */ @@ -257,14 +256,14 @@ export function stringSliceFlag( options?: { readonly alias?: string }, ) { const alias = options?.alias; - const pflagName = alias === undefined ? `--${name}` : `-${alias}, --${name}`; + const flagLabel = alias === undefined ? `--${name}` : `-${alias}, --${name}`; const base = Flag.String(name).pipe(Flag.withDescription(description), Flag.atLeast(0)); return (alias === undefined ? base : base.pipe(Flag.withAlias(alias))).pipe( Flag.mapTryCatch( (rawValues) => parseStringSliceFlag(rawValues), (err) => err instanceof StringSliceFlagParseError - ? `invalid argument ${JSON.stringify(err.value)} for "${pflagName}" flag: ${err.message}` + ? `invalid argument ${JSON.stringify(err.value)} for "${flagLabel}" flag: ${err.message}` : err instanceof Error ? err.message : String(err), diff --git a/apps/cli/src/command-internal/string-slice-flag.unit.test.ts b/apps/cli/src/command-internal/string-slice-flag.unit.test.ts index 7d70f88533..438e1bf775 100644 --- a/apps/cli/src/command-internal/string-slice-flag.unit.test.ts +++ b/apps/cli/src/command-internal/string-slice-flag.unit.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { parseStringSliceFlag, StringSliceFlagParseError } from "./string-slice-flag.ts"; -describe("parseStringSliceFlag (pflag StringSlice CSV parity)", () => { +describe("parseStringSliceFlag", () => { it("splits unquoted comma-separated values", () => { expect(parseStringSliceFlag(["public,private"])).toEqual(["public", "private"]); }); @@ -34,13 +34,13 @@ describe("parseStringSliceFlag (pflag StringSlice CSV parity)", () => { expect(parseStringSliceFlag([])).toEqual([]); }); - it("preserves whitespace (Go does not trim)", () => { + it("preserves whitespace (no trimming)", () => { expect(parseStringSliceFlag([" public , private "])).toEqual([" public ", " private "]); }); // Columns are 1-based byte offsets. - it("throws on an unterminated quoted field (column = byte length + 1, Go hits EOF)", () => { + it("throws on an unterminated quoted field (column = byte length + 1, reached EOF)", () => { expect(() => parseStringSliceFlag(['"tenant'])).toThrow(StringSliceFlagParseError); expect(() => parseStringSliceFlag(['"tenant'])).toThrow( 'parse error on line 1, column 8: extraneous or missing " in quoted-field', @@ -74,7 +74,7 @@ describe("parseStringSliceFlag (pflag StringSlice CSV parity)", () => { ); }); - it("counts columns in bytes, not code points (Go csv tracks byte offsets)", () => { + it("counts columns in bytes, not code points", () => { // é is 2 UTF-8 bytes, so the bare quote is at byte 3. expect(() => parseStringSliceFlag(['é"x'])).toThrow( 'parse error on line 1, column 3: bare " in non-quoted-field', @@ -85,7 +85,7 @@ describe("parseStringSliceFlag (pflag StringSlice CSV parity)", () => { ); }); - it("carries the offending occurrence and Go's exact message on the error for pflag framing", () => { + it("carries the offending occurrence and the exact message on the error for flag framing", () => { try { parseStringSliceFlag(["public", '"broken']); expect.unreachable("expected parseStringSliceFlag to throw"); @@ -106,7 +106,7 @@ describe("parseStringSliceFlag (pflag StringSlice CSV parity)", () => { // Only the first CSV record survives; blank lines before it are skipped, // `\r\n` normalizes to `\n`, and parse errors report per-line byte columns. - it("keeps only the first record when an unquoted newline ends it (pflag reads ONE record)", () => { + it("keeps only the first record when an unquoted newline ends it (only ONE record is read)", () => { expect(parseStringSliceFlag(["1.2.3.4\n5.6.7.8"])).toEqual(["1.2.3.4"]); // The dropped remainder is itself malformed CSV, but only the first record is parsed. expect(parseStringSliceFlag(['1.2.3.4\na"b'])).toEqual(["1.2.3.4"]); @@ -116,7 +116,7 @@ describe("parseStringSliceFlag (pflag StringSlice CSV parity)", () => { expect(parseStringSliceFlag(["a\r\nb"])).toEqual(["a"]); }); - it("skips blank lines before the record and errors with pflag's EOF for blank-only values", () => { + it("skips blank lines before the record and errors with EOF for blank-only values", () => { expect(parseStringSliceFlag(["\n\na"])).toEqual(["a"]); expect(parseStringSliceFlag(["\r\na"])).toEqual(["a"]); // A value of only blank lines throws the bare `EOF` message. diff --git a/apps/cli/src/command-internal/rune-width.ts b/apps/cli/src/command-internal/string-width.ts similarity index 98% rename from apps/cli/src/command-internal/rune-width.ts rename to apps/cli/src/command-internal/string-width.ts index 110dbb7fb8..ac73acf2c2 100644 --- a/apps/cli/src/command-internal/rune-width.ts +++ b/apps/cli/src/command-internal/string-width.ts @@ -376,7 +376,7 @@ function inRanges(cp: number, ranges: ReadonlyArray): boolean { } /** Display width of a single code point (0, 1, or 2). */ -function runeWidth(cp: number): number { +function codePointWidth(cp: number): number { // C0/C1 controls (except those handled by the caller) have no print width. if (cp === 0) return 0; if (cp < 0x20 || (cp >= 0x7f && cp < 0xa0)) return 0; @@ -388,6 +388,6 @@ function runeWidth(cp: number): number { /** Display width of a string, summing per-code-point widths. */ export function stringWidth(text: string): number { let width = 0; - for (const ch of text) width += runeWidth(ch.codePointAt(0)!); + for (const ch of text) width += codePointWidth(ch.codePointAt(0)!); return width; } diff --git a/apps/cli/src/command-internal/rune-width.unit.test.ts b/apps/cli/src/command-internal/string-width.unit.test.ts similarity index 88% rename from apps/cli/src/command-internal/rune-width.unit.test.ts rename to apps/cli/src/command-internal/string-width.unit.test.ts index 92d4cb21f0..44ea607d25 100644 --- a/apps/cli/src/command-internal/rune-width.unit.test.ts +++ b/apps/cli/src/command-internal/string-width.unit.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { stringWidth } from "./rune-width.ts"; +import { stringWidth } from "./string-width.ts"; describe("stringWidth", () => { it("counts ASCII as 1 each", () => { @@ -25,7 +25,7 @@ describe("stringWidth", () => { }); it("treats East Asian Ambiguous as width 1 (modern-terminal default)", () => { - // U+00A1 (¡) is Ambiguous; Go's runewidth with EastAsianWidth=false counts it as 1. + // U+00A1 (¡) is Ambiguous and counts as 1. expect(stringWidth("¡")).toBe(1); }); }); diff --git a/apps/cli/src/command-internal/go-struct-output.encoders.ts b/apps/cli/src/command-internal/struct-output.encoders.ts similarity index 74% rename from apps/cli/src/command-internal/go-struct-output.encoders.ts rename to apps/cli/src/command-internal/struct-output.encoders.ts index 554d655de7..9d193eca49 100644 --- a/apps/cli/src/command-internal/go-struct-output.encoders.ts +++ b/apps/cli/src/command-internal/struct-output.encoders.ts @@ -8,76 +8,75 @@ import { } from "../shared/telemetry/error-actionability.ts"; /** - * Byte-faithful `-o yaml`/`-o toml` output for struct payloads, matching `gopkg.in/yaml.v3` and - * `github.com/BurntSushi/toml`. Neither library reads `json:` tags, so emitted keys are the - * original Go field names, not the snake_case JSON the API returns. + * Byte-stable `-o yaml`/`-o toml` output for struct payloads. Emitted keys are the + * PascalCase field names declared by the shape, not the snake_case JSON the API returns. * - * Each payload family declares a {@link GoType} spec mirroring the original struct so the decoded - * JSON can be re-expressed with each library's own casing, nil handling, and quoting rules. + * Each payload family declares a {@link OutputShape} spec describing the struct so the decoded + * JSON can be re-expressed with the established casing, null handling, and quoting rules. */ -export type GoType = +export type OutputShape = | { readonly kind: "string" } | { readonly kind: "uuid" } | { readonly kind: "bool" } | { readonly kind: "int" } | { readonly kind: "float"; readonly bits: 32 | 64 } - /** Go `time.Time` — native TOML datetime, unquoted yaml timestamp. */ + /** RFC3339 timestamp — native TOML datetime, unquoted yaml timestamp. */ | { readonly kind: "time" } - /** Go `interface{}` — shape inferred from the JSON value like `encoding/json` decoding. */ + /** Untyped value — shape inferred from the JSON value. */ | { readonly kind: "any" } - | { readonly kind: "ptr"; readonly elem: GoType } - /** oapi-codegen `nullable.Nullable[T]` — a `map[bool]T` under the hood. */ - | { readonly kind: "nullable"; readonly elem: GoType } - | { readonly kind: "slice"; readonly elem: GoType } - | { readonly kind: "map"; readonly value: GoType } - | { readonly kind: "struct"; readonly fields: ReadonlyArray }; - -interface GoStructField { + | { readonly kind: "ptr"; readonly elem: OutputShape } + /** A nullable API field: absent, explicit null, or a value. */ + | { readonly kind: "nullable"; readonly elem: OutputShape } + | { readonly kind: "slice"; readonly elem: OutputShape } + | { readonly kind: "map"; readonly value: OutputShape } + | { readonly kind: "struct"; readonly fields: ReadonlyArray }; + +interface OutputShapeField { /** JSON tag name — the key present in the decoded payload. */ readonly json: string; - /** Go field name (PascalCase). */ - readonly go: string; - readonly type: GoType; + /** Struct field name (PascalCase). */ + readonly name: string; + readonly type: OutputShape; } -export const goString: GoType = { kind: "string" }; -export const goUuid: GoType = { kind: "uuid" }; -export const goBool: GoType = { kind: "bool" }; -export const goInt: GoType = { kind: "int" }; -export const goFloat32: GoType = { kind: "float", bits: 32 }; -export const goFloat64: GoType = { kind: "float", bits: 64 }; -export const goTime: GoType = { kind: "time" }; -export const goAny: GoType = { kind: "any" }; +export const shapeString: OutputShape = { kind: "string" }; +export const shapeUuid: OutputShape = { kind: "uuid" }; +export const shapeBool: OutputShape = { kind: "bool" }; +export const shapeInt: OutputShape = { kind: "int" }; +export const shapeFloat32: OutputShape = { kind: "float", bits: 32 }; +export const shapeFloat64: OutputShape = { kind: "float", bits: 64 }; +export const shapeTime: OutputShape = { kind: "time" }; +export const shapeAny: OutputShape = { kind: "any" }; -export function goPtr(elem: GoType): GoType { +export function shapePtr(elem: OutputShape): OutputShape { return { kind: "ptr", elem }; } -export function goNullable(elem: GoType): GoType { +export function shapeNullable(elem: OutputShape): OutputShape { return { kind: "nullable", elem }; } -export function goSlice(elem: GoType): GoType { +export function shapeSlice(elem: OutputShape): OutputShape { return { kind: "slice", elem }; } -export function goMap(value: GoType): GoType { +export function shapeMap(value: OutputShape): OutputShape { return { kind: "map", value }; } /** - * A struct field spec entry: `[jsonName, type]` derives the Go field name mechanically (each - * snake_case token capitalized: `api_key` → `ApiKey`), or `[jsonName, type, goName]` for explicit + * A struct field spec entry: `[jsonName, type]` derives the field name mechanically (each + * snake_case token capitalized: `api_key` → `ApiKey`), or `[jsonName, type, fieldName]` for explicit * names. */ -export type GoFieldSpec = - | readonly [json: string, type: GoType] - | readonly [json: string, type: GoType, goName: string]; +export type OutputShapeFieldSpec = + | readonly [json: string, type: OutputShape] + | readonly [json: string, type: OutputShape, fieldName: string]; -export function goStruct(fields: ReadonlyArray): GoType { +export function shapeStruct(fields: ReadonlyArray): OutputShape { return { kind: "struct", - fields: fields.map(([json, type, goName]) => ({ + fields: fields.map(([json, type, fieldName]) => ({ json, - go: goName ?? goFieldName(json), + name: fieldName ?? pascalCaseFieldName(json), type, })), }; @@ -85,57 +84,61 @@ export function goStruct(fields: ReadonlyArray): GoType { /** * The anonymous wrapper struct list commands use for TOML output (a `toml:` tag keeps the wrapper - * key lowercase while the elements keep Go field names), and also models a single-key map wrapper + * key lowercase while the elements keep PascalCase field names), and also models a single-key map wrapper * (`sso list`), since a one-field lowercase-keyed struct renders identically to a one-key map in * both encoders. */ -export function goTomlListWrapper(key: string, elem: GoType): GoType { - return { kind: "struct", fields: [{ json: key, go: key, type: goSlice(elem) }] }; +export function shapeTomlListWrapper(key: string, elem: OutputShape): OutputShape { + return { kind: "struct", fields: [{ json: key, name: key, type: shapeSlice(elem) }] }; } /** `api_key` → `ApiKey`, `dbAllowedCidrs` → `DbAllowedCidrs`. */ -export function goFieldName(jsonName: string): string { +export function pascalCaseFieldName(jsonName: string): string { return jsonName .split("_") .map((part) => (part.length === 0 ? part : part[0]?.toUpperCase() + part.slice(1))) .join(""); } -type GoValue = +type NormalizedValue = | { readonly k: "nil" } | { readonly k: "str"; readonly v: string } | { readonly k: "bool"; readonly v: boolean } | { readonly k: "int"; readonly v: number } | { readonly k: "float"; readonly v: number; readonly bits: 32 | 64 } | { readonly k: "time"; readonly v: string } - | { readonly k: "struct"; readonly entries: ReadonlyArray } + | { readonly k: "struct"; readonly entries: ReadonlyArray } | { readonly k: "map"; readonly nil: boolean; - readonly entries: ReadonlyArray; + readonly entries: ReadonlyArray; + } + /** A nullable field: absent (nil), `{false: zero}` (explicit null) or `{true: value}`. */ + | { + readonly k: "nullable"; + readonly present: boolean | undefined; + readonly value?: NormalizedValue; } - /** `nullable.Nullable[T]`: nil map, `{false: zero}` (explicit null) or `{true: value}`. */ - | { readonly k: "nullable"; readonly present: boolean | undefined; readonly value?: GoValue } | { readonly k: "slice"; readonly nil: boolean; - readonly items: ReadonlyArray; + readonly items: ReadonlyArray; readonly tables: boolean; }; -const GO_ZERO_TIME = "0001-01-01T00:00:00Z"; -const GO_ZERO_UUID = "00000000-0000-0000-0000-000000000000"; +const ZERO_TIME = "0001-01-01T00:00:00Z"; +const ZERO_UUID = "00000000-0000-0000-0000-000000000000"; function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } -function zeroValue(type: GoType): GoValue { +function zeroValue(type: OutputShape): NormalizedValue { switch (type.kind) { case "string": return { k: "str", v: "" }; case "uuid": - return { k: "str", v: GO_ZERO_UUID }; + return { k: "str", v: ZERO_UUID }; case "bool": return { k: "bool", v: false }; case "int": @@ -143,7 +146,7 @@ function zeroValue(type: GoType): GoValue { case "float": return { k: "float", v: 0, bits: type.bits }; case "time": - return { k: "time", v: GO_ZERO_TIME }; + return { k: "time", v: ZERO_TIME }; case "struct": return normalize(undefined, type); case "ptr": @@ -158,7 +161,7 @@ function zeroValue(type: GoType): GoValue { } } -function elementsAreTables(elem: GoType, items: ReadonlyArray): boolean { +function elementsAreTables(elem: OutputShape, items: ReadonlyArray): boolean { switch (elem.kind) { case "struct": case "map": @@ -168,15 +171,14 @@ function elementsAreTables(elem: GoType, items: ReadonlyArray): boolean case "slice": return elem.kind === "ptr" ? elementsAreTables(elem.elem, items) : false; case "any": - // Like Go's runtime type inspection: JSON objects decode to - // map[string]interface{} which BurntSushi treats as tables. + // JSON objects decode to maps, which are rendered as tables. return items.length > 0 && items.every(isRecord); default: return false; } } -function normalize(value: unknown, type: GoType): GoValue { +function normalize(value: unknown, type: OutputShape): NormalizedValue { switch (type.kind) { case "string": return typeof value === "string" ? { k: "str", v: value } : zeroValue(type); @@ -196,13 +198,12 @@ function normalize(value: unknown, type: GoType): GoValue { : zeroValue(type); case "time": return typeof value === "string" && value.length > 0 - ? { k: "time", v: normalizeGoTime(value) } + ? { k: "time", v: normalizeTime(value) } : zeroValue(type); case "ptr": return value === undefined || value === null ? { k: "nil" } : normalize(value, type.elem); case "nullable": - // oapi-codegen: absent key → nil map; explicit JSON null → {false: zero}; - // value → {true: value}. + // Absent key → nil; explicit JSON null → {false: zero}; value → {true: value}. if (value === undefined) return { k: "nullable", present: undefined }; if (value === null) return { k: "nullable", present: false, value: zeroValue(type.elem) }; return { k: "nullable", present: true, value: normalize(value, type.elem) }; @@ -230,7 +231,7 @@ function normalize(value: unknown, type: GoType): GoValue { return { k: "struct", entries: type.fields.map( - (field) => [field.go, normalize(record[field.json], field.type)] as const, + (field) => [field.name, normalize(record[field.json], field.type)] as const, ), }; } @@ -239,13 +240,13 @@ function normalize(value: unknown, type: GoType): GoValue { } } -/** Mirror `encoding/json` decoding into `interface{}`. */ -function normalizeAny(value: unknown): GoValue { +/** Normalize a decoded JSON value into an untyped value. */ +function normalizeAny(value: unknown): NormalizedValue { if (value === undefined || value === null) return { k: "nil" }; if (typeof value === "string") return { k: "str", v: value }; if (typeof value === "boolean") return { k: "bool", v: value }; if (typeof value === "number") { - // JSON numbers decode to float64 in Go's interface{} world. + // JSON numbers decode to float64. return { k: "float", v: value, bits: 64 }; } if (Array.isArray(value)) { @@ -267,13 +268,12 @@ function normalizeAny(value: unknown): GoValue { } /** - * Formats an RFC3339 input the way Go renders a decoded `time.Time` with `time.RFC3339Nano`: the + * Formats an RFC3339 input in `RFC3339Nano` form: the * fraction truncated (not rounded) to at most 9 digits, then trailing zeros trimmed and a zero * offset rendered as `Z`. */ -function normalizeGoTime(value: string): string { - // Both `.` and `,` are accepted as the fractional separator on decode; normalize to the dot Go - // emits. +function normalizeTime(value: string): string { + // Both `.` and `,` are accepted as the fractional separator on decode; normalize to the dot. const match = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})([.,]\d+)?(Z|[+-]\d{2}:\d{2})$/.exec(value); if (match === null) return value; const [, base, fraction, offset] = match; @@ -287,10 +287,9 @@ function normalizeGoTime(value: string): string { } /** - * Shortest round-trip digits for a float32 value, matching `strconv.FormatFloat(f, 'g', -1, 32)`'s - * Ryu algorithm: the fewest significant digits that parse back to the same float32, with an exact + * Shortest round-trip digits for a float32 value (Ryu algorithm): the fewest significant digits that parse back to the same float32, with an exact * decimal tie rounded to the even final digit (unlike JS `toPrecision`, which rounds half up). - * Returns `e<±exp>` for {@link goFormatFloat}. + * Returns `e<±exp>` for {@link formatStructFloat}. */ function shortestFloat32(value: number): string { const rounded = Math.fround(value); @@ -359,16 +358,15 @@ function roundDecimalDigits( } /** - * `strconv.FormatFloat(f, 'g', -1, bits)`: shortest digits, switching to - * scientific notation when the decimal exponent is < -4 or >= 6 (Go uses - * `eprec = 6` for shortest formatting), with a sign and >= 2 exponent digits. + * Shortest round-trip digits, switching to scientific notation when the decimal exponent is + * < -4 or >= 6, with a sign and >= 2 exponent digits. */ -export function goFormatFloat(value: number, bits: 32 | 64): string { +export function formatStructFloat(value: number, bits: 32 | 64): string { if (Number.isNaN(value)) return "NaN"; if (value === Infinity) return "+Inf"; if (value === -Infinity) return "-Inf"; const repr = bits === 32 ? shortestFloat32(value) : String(value); - // JS String(-0) drops the sign; Go's FormatFloat keeps it ("-0"). + // JS String(-0) drops the sign; the output keeps it ("-0"). const negative = repr.startsWith("-") || Object.is(value, -0); const unsigned = negative ? repr.slice(1) : repr; // Decompose into digits + decimal exponent. @@ -405,14 +403,13 @@ export function goFormatFloat(value: number, bits: 32 | 64): string { } /** - * Encode a decoded payload as the Go CLI's `-o yaml` output for the given Go - * struct spec. Returns the full document bytes (trailing newline included). + * Encode a decoded payload as `-o yaml` output for the given struct spec. Returns the full document bytes (trailing newline included). */ -export function encodeGoYaml(value: unknown, type: GoType): string { +export function encodeStructYaml(value: unknown, type: OutputShape): string { return yamlDocument(normalize(value, type)); } -function yamlDocument(root: GoValue): string { +function yamlDocument(root: NormalizedValue): string { switch (root.k) { case "slice": if (root.items.length === 0) return "[]\n"; @@ -433,43 +430,40 @@ function yamlDocument(root: GoValue): string { } /** - * A populated `nullable.Nullable[T]` is a `map[bool]T`; yaml.v3 renders the - * bool key plain (`true:` / `false:`), unlike the string keys `"true"` would - * produce. + * A populated nullable renders as a mapping with a plain bool key (`true:` / `false:`), + * unlike the quoted string keys `"true"` would produce. */ -function yamlNullableBlock(present: boolean, value: GoValue, indent: number): string { +function yamlNullableBlock(present: boolean, value: NormalizedValue, indent: number): string { const pad = " ".repeat(indent); return `${pad}${present ? "true" : "false"}:${yamlValueSuffix(value, indent)}`; } -/** yaml.v3's indent algorithm: children of a mapping align to the next 4-column stop. */ +/** Children of a mapping align to the next 4-column stop. */ function yamlNextIndent(indent: number): number { return 4 * Math.floor((indent + 4) / 4); } function yamlStructEntries( - entries: ReadonlyArray, -): ReadonlyArray { - // yaml.v3 lowercases Go field names wholesale (no yaml tags on these structs). - return entries.map(([go, value]) => [go.toLowerCase(), value] as const); + entries: ReadonlyArray, +): ReadonlyArray { + // Field names are lowercased wholesale in YAML output. + return entries.map(([name, value]) => [name.toLowerCase(), value] as const); } function yamlMapEntries( - entries: ReadonlyArray, -): ReadonlyArray { + entries: ReadonlyArray, +): ReadonlyArray { return [...entries].sort(([a], [b]) => (yamlKeyLess(a, b) ? -1 : yamlKeyLess(b, a) ? 1 : 0)); } /** - * Unicode code-point string ordering, matching both `sort.Strings` (UTF-8 byte order) and - * yaml.v3's `keyList.Less` (rune order) — the two are equivalent, and both differ from JS `<` - * (UTF-16 code-unit order) when an astral character meets a high-BMP one (Go sorts U+E000 before - * U+1F600, UTF-16 the reverse). + * Unicode code-point string ordering (equivalent to UTF-8 byte order), which differs from JS `<` + * (UTF-16 code-unit order) when an astral character meets a high-BMP one (code-point order sorts + * U+E000 before U+1F600, UTF-16 the reverse). * - * Also used by `go-output.encoders.ts`'s `sortKeysDeep`, since `encoding/json`'s map-key sort is - * the same order. + * Also used by `output.encoders.ts`'s `sortKeysDeep`. */ -export function goStringCompare(a: string, b: string): number { +export function compareByCodepoint(a: string, b: string): number { let i = 0; while (i < a.length && i < b.length) { const ac = a.codePointAt(i) as number; @@ -481,8 +475,8 @@ export function goStringCompare(a: string, b: string): number { } /** - * yaml.v3's `keyList.Less` natural string ordering. Digit runs use `unicode.IsDigit` (any Unicode - * `Nd` digit), so a non-ASCII digit like Arabic-Indic sorts by its raw code point and ends up + * Natural string ordering: runs of digits compare numerically. Digit runs match any Unicode + * `Nd` digit, so a non-ASCII digit like Arabic-Indic sorts by its raw code point and ends up * after ASCII digit runs. The ASCII-only {@link isDigit} stays for the scalar parser. */ function yamlKeyLess(a: string, b: string): boolean { @@ -498,7 +492,7 @@ function yamlKeyLess(a: string, b: string): boolean { } const al = isLetter(ac); const bl = isLetter(bc); - // Go compares runes (`ar[i] < br[i]`), i.e. code points, not UTF-16 units. + // Compare code points, not UTF-16 units. if (al && bl) return (ac.codePointAt(0) as number) < (bc.codePointAt(0) as number); if (al || bl) return digits ? al : bl; let an = 0n; @@ -514,7 +508,7 @@ function yamlKeyLess(a: string, b: string): boolean { } let ai = i; let bi = i; - // Go accumulates into `int64` without overflow checks, so a 19+-digit run wraps negative and + // Digit runs accumulate into `int64` without overflow checks, so a 19+-digit run wraps negative and // sorts before a shorter positive run. `BigInt.asIntN(64, …)` reproduces the wrap. for (; ai < ar.length && isSortDigit(ar[ai] as string); ai++) { an = BigInt.asIntN(64, an * 10n + BigInt(((ar[ai] as string).codePointAt(0) as number) - 48)); @@ -529,7 +523,7 @@ function yamlKeyLess(a: string, b: string): boolean { return ar.length < br.length; } -/** yaml.v3 sorter's `unicode.IsDigit` — any Unicode decimal digit (`Nd`). */ +/** Any Unicode decimal digit (`Nd`), as used by the key sorter. */ function isSortDigit(c: string): boolean { return /\p{Nd}/u.test(c); } @@ -542,7 +536,10 @@ function isLetter(c: string): boolean { return /\p{L}/u.test(c); } -function yamlMapping(entries: ReadonlyArray, indent: number): string { +function yamlMapping( + entries: ReadonlyArray, + indent: number, +): string { const pad = " ".repeat(indent); let out = ""; for (const [key, value] of entries) { @@ -556,7 +553,7 @@ function yamlMapping(entries: ReadonlyArray, indent: * Everything after `key:` — either ` \n`, a block-literal header plus * content lines, or `\n` plus an indented child block. */ -function yamlValueSuffix(value: GoValue, indent: number): string { +function yamlValueSuffix(value: NormalizedValue, indent: number): string { switch (value.k) { case "nil": return " null\n"; @@ -591,7 +588,7 @@ function yamlValueSuffix(value: GoValue, indent: number): string { } } -function yamlSequence(items: ReadonlyArray, indent: number): string { +function yamlSequence(items: ReadonlyArray, indent: number): string { const pad = " ".repeat(indent); let out = ""; for (const item of items) { @@ -605,7 +602,7 @@ function yamlSequence(items: ReadonlyArray, indent: number): string { break; } // Compact form: the first key rides on the `- ` line; the block keeps - // a +2 indent (yaml.v3 special-cases indent inside sequence items). + // a +2 indent (indent inside sequence items is special-cased). const block = yamlMapping(entries, indent + 2); out += `${pad}- ${block.slice(indent + 2)}`; break; @@ -644,7 +641,7 @@ function yamlSequence(items: ReadonlyArray, indent: number): string { return out; } -function yamlScalar(value: GoValue): string { +function yamlScalar(value: NormalizedValue): string { switch (value.k) { case "nil": return "null"; @@ -653,7 +650,7 @@ function yamlScalar(value: GoValue): string { case "int": return String(value.v); case "float": - return goFormatFloat(value.v, value.bits); + return formatStructFloat(value.v, value.bits); case "time": return value.v; case "str": { @@ -675,7 +672,7 @@ function yamlKeyScalar(key: string): string { type YamlStringStyle = "plain" | "single" | "double" | "literal"; /** - * yaml.v3's style selection: literal for multi-line strings, double quotes for a string that + * Style selection: literal for multi-line strings, double quotes for a string that * resolves to a non-string tag, otherwise downgraded from plain to single (or double) based on * scalar analysis. */ @@ -695,7 +692,7 @@ function yamlStringStyle(s: string): YamlStringStyle { } /** - * Characters yaml.v3 treats as "special" (not printable) or line breaks other + * Characters treated as "special" (not printable) or line breaks other * than `\n` — all of these force double-quoted style with escapes. U+2028 and * U+2029 are technically YAML line breaks, but every realistic payload * containing them round-trips through the double-quoted `\L` / `\P` escapes. @@ -711,8 +708,7 @@ function yamlHasSpecialChars(s: string): boolean { } /** - * libyaml's `is_printable` in code-point terms. The byte-oriented original never accepts a 4-byte - * UTF-8 lead, so every astral character — along with C0/C1 controls, DEL, surrogates, the U+FEFF + * YAML printability in code-point terms: every astral character — along with C0/C1 controls, DEL, surrogates, the U+FEFF * BOM, and U+FFFE/U+FFFF — is "not printable" and gets double-quoted escapes. */ function yamlIsPrintable(code: number): boolean { @@ -738,7 +734,7 @@ function yamlPlainDisallowed(s: string): boolean { } /** - * Would yaml.v3's `resolve("", s)` produce a non-string tag? Also covers the + * Would the plain scalar `s` resolve to a non-string YAML tag? Also covers the * YAML 1.1 "old bool" and base-60 spellings the encoder force-quotes. */ function yamlResolvesToString(s: string): boolean { @@ -747,15 +743,15 @@ function yamlResolvesToString(s: string): boolean { if (YAML_BASE60.test(s)) return false; const first = s[0] as string; if (first === ".") { - // strconv.ParseFloat errors on overflow (±Inf), so an overflowing spelling like `1e999` stays + // Float parsing errors on overflow (±Inf), so an overflowing spelling like `1e999` stays // a string and needs no quoting. return !(/^\.\d+(?:[eE][+-]?\d+)?$/.test(s) && Number.isFinite(Number(s))); } if (first === "+" || first === "-" || isDigit(first)) { if (yamlIsTimestamp(s)) return false; const plain = s.replaceAll("_", ""); - if (goParseIntBase0(plain)) return false; - // An overflowing float (→ ±Inf) is a ParseFloat error, so it resolves as a string and stays + if (parsesAsBaseZeroInt(plain)) return false; + // An overflowing float (→ ±Inf) is a float parse error, so it resolves as a string and stays // plain; an underflowing one (1e-999 → 0) succeeds and stays float-tagged, hence quoted. // `Number` mirrors the accepted shapes since YAML_STYLE_FLOAT gates the syntax first. if (YAML_STYLE_FLOAT.test(plain) && Number.isFinite(Number(plain))) return false; @@ -812,8 +808,8 @@ const YAML_RESOLVE_MAP = new Set([ const YAML_BASE60 = /^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+(?:\.[0-9_]*)?$/; const YAML_STYLE_FLOAT = /^[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?$/; -/** `strconv.ParseInt(s, 0, 64)` / `ParseUint` success (underscores pre-stripped). */ -function goParseIntBase0(plain: string): boolean { +/** Whether `plain` parses as a base-0 int64/uint64 (underscores pre-stripped). */ +function parsesAsBaseZeroInt(plain: string): boolean { let body = plain; let negative = false; if (body.startsWith("+") || body.startsWith("-")) { @@ -846,20 +842,18 @@ function goParseIntBase0(plain: string): boolean { } else { return false; } - // resolve() falls back from ParseInt to ParseUint, so the accepted range is + // Resolution falls back from int64 to uint64, so the accepted range is // [-2^63, 2^64) — anything beyond either bound is not an int. if (negative) return parsed <= 9223372036854775808n; return parsed < 18446744073709551616n; } /** - * yaml.v3's `parseTimestamp` layouts, which delegate to `time.Parse` — so calendar dates and zone - * offsets are validated exactly like Go's time package (`2025-02-31` and `2100-02-29` stay plain, - * `2024-02-29` is a timestamp). + * Timestamp layouts with validated calendar dates and zone offsets (`2025-02-31` and + * `2100-02-29` stay plain, `2024-02-29` is a timestamp). */ function yamlIsTimestamp(s: string): boolean { - // The fraction separator is `.` or `,` — yaml.v3 resolves timestamps through `time.Parse`, - // which accepts either. + // The fraction separator is `.` or `,`. const match = /^(\d{4})-(\d{1,2})-(\d{1,2})(?:([Tt ])(\d{1,2}):(\d{1,2}):(\d{1,2})(?:[.,]\d+)?(Z|[+-]\d{2}:\d{2})?)?$/.exec( s, @@ -872,7 +866,7 @@ function yamlIsTimestamp(s: string): boolean { if (separator !== " " && offset === undefined) return false; if (Number(hour) > 23 || Number(minute) > 59 || Number(second) > 59) return false; } - // time.Parse's zone-offset range checks: the hour is rejected above 24 and the minute above 60 + // Zone-offset range checks: the hour is rejected above 24 and the minute above 60 // — `+24:59` and `+00:60` are accepted, `+25:00` and `+23:99` are not. if (offset !== undefined && offset !== "Z") { if (Number(offset.slice(1, 3)) > 24 || Number(offset.slice(4, 6)) > 60) return false; @@ -880,12 +874,12 @@ function yamlIsTimestamp(s: string): boolean { const month = Number(monthRaw); const day = Number(dayRaw); if (month < 1 || month > 12) return false; - if (day < 1 || day > goDaysInMonth(Number(yearRaw), month)) return false; + if (day < 1 || day > daysInMonth(Number(yearRaw), month)) return false; return true; } -/** `time.Parse`'s "day out of range" bound (`daysIn`, proleptic Gregorian). */ -function goDaysInMonth(year: number, month: number): number { +/** Days in a month of the proleptic Gregorian calendar. */ +function daysInMonth(year: number, month: number): number { if (month === 2) { const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); return leap ? 29 : 28; @@ -952,8 +946,7 @@ function yamlDoubleQuoted(s: string): string { out += "\\P"; break; default: - // Non-printables escape by rune width like yaml.v3's double-quoted - // writer: `\xXX`, `\uXXXX`, or `\U00XXXXXX` with uppercase hex. + // Non-printables escape as `\xXX`, `\uXXXX`, or `\U00XXXXXX` with uppercase hex. if (yamlIsPrintable(code)) { out += ch; } else if (code <= 0xff) { @@ -973,10 +966,8 @@ function yamlDoubleQuoted(s: string): string { * explicit `4` indentation indicator when the first line starts with a space * or is empty, and content indented to the next 4-column stop. * - * Unlike Go's streaming bufio-backed encoder (which can flush partial output - * before a later error), this builds the whole document in memory — callers - * emit all-or-nothing, which only differs observably from Go on multi-KB - * payloads that fail mid-encode. + * Builds the whole document in memory — callers emit all-or-nothing, so no partial output + * is flushed when a later field fails mid-encode. */ function yamlBlockLiteral(s: string, indent: number): string { const contentIndent = yamlNextIndent(indent); @@ -986,7 +977,7 @@ function yamlBlockLiteral(s: string, indent: number): string { const indicator = s.startsWith(" ") || s.startsWith("\n") ? "4" : ""; const lines = s.split("\n"); if (s.endsWith("\n")) lines.pop(); - // yaml.v3 merges a leading empty line's break with the header newline (e.g. "\nx" → `|4-\n + // A leading empty line's break merges with the header newline (e.g. "\nx" → `|4-\n // x\n`). if (lines[0] === "") lines.shift(); const body = lines.map((line) => (line.length === 0 ? "" : `${pad}${line}`)).join("\n"); @@ -994,14 +985,14 @@ function yamlBlockLiteral(s: string, indent: number): string { } /** - * Thrown when BurntSushi would refuse the payload: a populated - * `nullable.Nullable` field (`map[bool]T` has a non-string key type — observed - * on `snippets list -o toml`) or a `nil` element inside an inline array. + * Thrown when the payload has no TOML form: a populated nullable field (its bool key is not a + * valid TOML key — observed on `snippets list -o toml`) or a null element inside an inline array. */ -export class GoTomlEncodeError extends Data.Error<{ readonly message: string }> { +export class TomlEncodeError extends Data.Error<{ readonly message: string }> { + // The fingerprint and `name` are the stable telemetry/output identity of this error. static readonly [ErrorActionabilityFingerprintId] = "GoTomlEncodeError"; override readonly name = "GoTomlEncodeError"; - constructor(message = "toml: cannot encode a map with non-string key type") { + constructor(message = "cannot encode a map with non-string keys") { super({ message }); } @@ -1011,15 +1002,13 @@ export class GoTomlEncodeError extends Data.Error<{ readonly message: string }> } /** - * Encodes a decoded payload as the Go CLI's `-o toml` output. Returns the full document, which can - * be empty (BurntSushi emits nothing for an all-nil payload). Throws {@link GoTomlEncodeError} to - * match Go's runtime failure on a populated nullable field. + * Encodes a decoded payload as `-o toml` output. Returns the full document, which can + * be empty (an all-nil payload emits nothing). Throws {@link TomlEncodeError} on + * a populated nullable field. * - * On the error path only, Go's real stdout can already contain partial buffered output that this - * in-memory port doesn't reproduce; emit nothing on error rather than the accumulated prefix, - * which would emit more than Go does. + * Emits nothing on error rather than the accumulated prefix. */ -export function encodeGoToml(value: unknown, type: GoType): string { +export function encodeStructToml(value: unknown, type: OutputShape): string { const state = { out: "", hasWritten: false }; tomlEncode(state, [], normalize(value, type)); return state.out; @@ -1044,7 +1033,7 @@ function tomlIndent(key: ReadonlyArray): string { return " ".repeat(Math.max(key.length - 1, 0)); } -function tomlIsTable(value: GoValue): boolean { +function tomlIsTable(value: NormalizedValue): boolean { switch (value.k) { case "struct": return true; @@ -1053,15 +1042,14 @@ function tomlIsTable(value: GoValue): boolean { case "nullable": return true; case "slice": - // Array-of-tables only when non-empty with table elements (BurntSushi's - // isTableArray returns false for empty slices). + // Array-of-tables only when non-empty with table elements. return value.tables && value.items.length > 0; default: return false; } } -function tomlIsNil(value: GoValue): boolean { +function tomlIsNil(value: NormalizedValue): boolean { switch (value.k) { case "nil": return true; @@ -1076,7 +1064,7 @@ function tomlIsNil(value: GoValue): boolean { } } -function tomlEncode(state: TomlState, key: ReadonlyArray, value: GoValue): void { +function tomlEncode(state: TomlState, key: ReadonlyArray, value: NormalizedValue): void { if (tomlIsNil(value)) return; switch (value.k) { case "struct": @@ -1084,8 +1072,8 @@ function tomlEncode(state: TomlState, key: ReadonlyArray, value: GoValue tomlTable(state, key, value); return; case "nullable": - // Populated nullable.Nullable[T] is a map[bool]T — BurntSushi panics. - throw new GoTomlEncodeError(); + // A populated nullable has a bool key, which TOML cannot encode. + throw new TomlEncodeError(); case "slice": if (tomlIsTable(value)) { tomlArrayOfTables(state, key, value.items); @@ -1099,19 +1087,19 @@ function tomlEncode(state: TomlState, key: ReadonlyArray, value: GoValue } /** - * Map/struct entries in BurntSushi's write order: map entries sorted by - * {@link goStringCompare} (structs keep declaration order), then both + * Map/struct entries in TOML write order: map entries sorted by + * {@link compareByCodepoint} (structs keep declaration order), then both * partitioned into non-table ("direct") and table ("sub") groups via - * {@link tomlIsTable} — `eStruct`/`eMap` always write direct fields before + * {@link tomlIsTable} — direct fields are always written before * sub-tables. */ -function tomlOrderedEntries(value: Extract): { - direct: ReadonlyArray; - sub: ReadonlyArray; +function tomlOrderedEntries(value: Extract): { + direct: ReadonlyArray; + sub: ReadonlyArray; } { const entries = value.k === "map" - ? [...value.entries].sort(([a], [b]) => goStringCompare(a, b)) + ? [...value.entries].sort(([a], [b]) => compareByCodepoint(a, b)) : value.entries; return { direct: entries.filter(([, v]) => !tomlIsTable(v)), @@ -1122,7 +1110,7 @@ function tomlOrderedEntries(value: Extract): { function tomlTable( state: TomlState, key: ReadonlyArray, - value: Extract, + value: Extract, ): void { if (key.length === 1) { // Extra newline between top-level tables. @@ -1145,7 +1133,7 @@ function tomlTable( function tomlArrayOfTables( state: TomlState, key: ReadonlyArray, - items: ReadonlyArray, + items: ReadonlyArray, ): void { for (const item of items) { if (tomlIsNil(item)) continue; @@ -1158,12 +1146,12 @@ function tomlArrayOfTables( tomlEncode(state, [...key, name], v); } } else if (item.k === "nullable") { - throw new GoTomlEncodeError(); + throw new TomlEncodeError(); } } } -function tomlKeyValue(state: TomlState, key: ReadonlyArray, value: GoValue): void { +function tomlKeyValue(state: TomlState, key: ReadonlyArray, value: NormalizedValue): void { const name = key[key.length - 1] as string; tomlWrite(state, `${tomlIndent(key)}${tomlKeyName(name)} = ${tomlElement(value)}\n`); } @@ -1174,7 +1162,7 @@ function tomlKeyName(name: string): string { return TOML_BARE_KEY.test(name) ? name : tomlQuoted(name); } -function tomlElement(value: GoValue): string { +function tomlElement(value: NormalizedValue): string { switch (value.k) { case "str": return tomlQuoted(value.v); @@ -1183,7 +1171,7 @@ function tomlElement(value: GoValue): string { case "int": return String(value.v); case "float": { - const repr = goFormatFloat(value.v, value.bits); + const repr = formatStructFloat(value.v, value.bits); // TOML floats must carry a decimal point unless in exponent form. return repr.includes(".") || repr.includes("e") ? repr : `${repr}.0`; } @@ -1195,16 +1183,16 @@ function tomlElement(value: GoValue): string { case "map": return tomlInlineTable(value); case "nullable": - throw new GoTomlEncodeError(); + throw new TomlEncodeError(); case "nil": - // BurntSushi's `eElement` rejects nil inline-array elements (`[null, "x"]` fails), while nil + // Null inline-array elements are rejected (`[null, "x"]` fails), while null // map values are silently skipped. - throw new GoTomlEncodeError("toml: cannot encode array with nil element"); + throw new TomlEncodeError("cannot encode an array with a null element"); } } /** - * BurntSushi's inline-table form, used for map/struct elements of arrays that aren't + * The inline-table form, used for map/struct elements of arrays that aren't * arrays-of-tables: `{k = v, ...}` with nil entries skipped and non-table values before table * values, map keys byte-sorted within each group. * @@ -1212,14 +1200,14 @@ function tomlElement(value: GoValue): string { * written, so a skipped nil entry in the final position leaves a dangling `", "` (e.g. * `[{"10":1,"b":null},false]` → `[{10 = 1.0, }, false]`). */ -function tomlInlineTable(value: Extract): string { +function tomlInlineTable(value: Extract): string { let out = "{"; if (value.k === "map") { - const sorted = [...value.entries].sort(([a], [b]) => goStringCompare(a, b)); + const sorted = [...value.entries].sort(([a], [b]) => compareByCodepoint(a, b)); const direct = sorted.filter(([, v]) => !tomlIsTable(v)); const sub = sorted.filter(([, v]) => tomlIsTable(v)); const writeGroup = ( - group: ReadonlyArray, + group: ReadonlyArray, trailingComma: boolean, ): void => { for (const [index, [name, v]] of group.entries()) { @@ -1243,7 +1231,7 @@ function tomlInlineTable(value: Extract): stri return `${out}}`; } -/** BurntSushi's `dblQuotedReplacer` escape set. */ +/** The double-quoted TOML string escape set. */ function tomlQuoted(s: string): string { let out = '"'; for (const ch of s) { diff --git a/apps/cli/src/command-internal/go-struct-output.encoders.unit.test.ts b/apps/cli/src/command-internal/struct-output.encoders.unit.test.ts similarity index 50% rename from apps/cli/src/command-internal/go-struct-output.encoders.unit.test.ts rename to apps/cli/src/command-internal/struct-output.encoders.unit.test.ts index 0c8900aac7..93af2144df 100644 --- a/apps/cli/src/command-internal/go-struct-output.encoders.unit.test.ts +++ b/apps/cli/src/command-internal/struct-output.encoders.unit.test.ts @@ -1,52 +1,46 @@ import { describe, expect, it } from "vitest"; import { - GoTomlEncodeError, - encodeGoToml, - encodeGoYaml, - goAny, - goBool, - goFieldName, - goFloat32, - goFloat64, - goFormatFloat, - goInt, - goMap, - goNullable, - goPtr, - goSlice, - goString, - goStruct, - goTime, - goTomlListWrapper, - goUuid, -} from "./go-struct-output.encoders.ts"; - -/** - * Golden byte strings in this file were captured from a scratch Go program calling - * `utils.EncodeOutput`, using the same BurntSushi toml and yaml.v3 versions pinned in the - * reference `go.mod`. - */ + TomlEncodeError, + encodeStructToml, + encodeStructYaml, + shapeAny, + shapeBool, + pascalCaseFieldName, + shapeFloat32, + shapeFloat64, + formatStructFloat, + shapeInt, + shapeMap, + shapeNullable, + shapePtr, + shapeSlice, + shapeString, + shapeStruct, + shapeTime, + shapeTomlListWrapper, + shapeUuid, +} from "./struct-output.encoders.ts"; // Mirrors the branch response struct. -const BRANCH_RESPONSE = goStruct([ - ["created_at", goTime], - ["deletion_scheduled_at", goPtr(goTime)], - ["git_branch", goPtr(goString)], - ["id", goUuid], - ["is_default", goBool], - ["latest_check_run_id", goPtr(goFloat32)], - ["name", goString], - ["notify_url", goPtr(goString)], - ["parent_project_ref", goString], - ["persistent", goBool], - ["pr_number", goPtr(goInt)], - ["preview_project_status", goPtr(goString)], - ["project_ref", goString], - ["review_requested_at", goPtr(goTime)], - ["status", goString], - ["updated_at", goTime], - ["with_data", goBool], +const BRANCH_RESPONSE = shapeStruct([ + ["created_at", shapeTime], + ["deletion_scheduled_at", shapePtr(shapeTime)], + ["git_branch", shapePtr(shapeString)], + ["id", shapeUuid], + ["is_default", shapeBool], + ["latest_check_run_id", shapePtr(shapeFloat32)], + ["name", shapeString], + ["notify_url", shapePtr(shapeString)], + ["parent_project_ref", shapeString], + ["persistent", shapeBool], + ["pr_number", shapePtr(shapeInt)], + ["preview_project_status", shapePtr(shapeString)], + ["project_ref", shapeString], + ["review_requested_at", shapePtr(shapeTime)], + ["status", shapeString], + ["updated_at", shapeTime], + ["with_data", shapeBool], ]); const SAMPLE_BRANCH = { @@ -63,7 +57,7 @@ const SAMPLE_BRANCH = { with_data: true, }; -// All pointer fields absent — Go zero-fills the value fields. +// All pointer fields absent — the value fields are zero-filled. const ZERO_BRANCH = { name: "Production", is_default: true, @@ -72,10 +66,10 @@ const ZERO_BRANCH = { status: "FUNCTIONS_DEPLOYED", }; -describe("encodeGoToml", () => { - it("matches Go byte-for-byte for a branches list wrapper (PascalCase, nil pointers omitted, native datetimes)", () => { - const wrapper = goTomlListWrapper("branches", BRANCH_RESPONSE); - expect(encodeGoToml({ branches: [SAMPLE_BRANCH, ZERO_BRANCH] }, wrapper)).toBe( +describe("encodeStructToml", () => { + it("matches the golden output for a branches list wrapper (PascalCase, nil pointers omitted, native datetimes)", () => { + const wrapper = shapeTomlListWrapper("branches", BRANCH_RESPONSE); + expect(encodeStructToml({ branches: [SAMPLE_BRANCH, ZERO_BRANCH] }, wrapper)).toBe( `[[branches]] CreatedAt = 2026-05-27T01:02:03Z GitBranch = "feat-1" @@ -105,7 +99,7 @@ describe("encodeGoToml", () => { }); it("emits a top-level struct without a table header (branches create)", () => { - expect(encodeGoToml(SAMPLE_BRANCH, BRANCH_RESPONSE)).toBe( + expect(encodeStructToml(SAMPLE_BRANCH, BRANCH_RESPONSE)).toBe( `CreatedAt = 2026-05-27T01:02:03Z GitBranch = "feat-1" Id = "11111111-2222-3333-4444-555555555555" @@ -122,60 +116,60 @@ WithData = true }); it("emits nothing for a nil list and `key = []` for a decoded empty list", () => { - const wrapper = goTomlListWrapper("branches", BRANCH_RESPONSE); - // Go: `var result []api.BranchResponse` stays nil when empty → no output. - expect(encodeGoToml({ branches: undefined }, wrapper)).toBe(""); - // Go: a decoded `[]` is a non-nil empty slice → `branches = []`. - expect(encodeGoToml({ branches: [] }, wrapper)).toBe("branches = []\n"); + const wrapper = shapeTomlListWrapper("branches", BRANCH_RESPONSE); + // An absent list → no output. + expect(encodeStructToml({ branches: undefined }, wrapper)).toBe(""); + // A decoded `[]` is an empty list → `branches = []`. + expect(encodeStructToml({ branches: [] }, wrapper)).toBe("branches = []\n"); }); it("nests sub-tables after primitives with 2-space indentation (hostnames shape)", () => { // Mirrors the custom-hostname update response struct. - const spec = goStruct([ - ["custom_hostname", goString], + const spec = shapeStruct([ + ["custom_hostname", shapeString], [ "data", - goStruct([ - ["errors", goSlice(goAny)], - ["messages", goSlice(goAny)], + shapeStruct([ + ["errors", shapeSlice(shapeAny)], + ["messages", shapeSlice(shapeAny)], [ "result", - goStruct([ - ["custom_origin_server", goString], - ["hostname", goString], - ["id", goString], + shapeStruct([ + ["custom_origin_server", shapeString], + ["hostname", shapeString], + ["id", shapeString], [ "ownership_verification", - goStruct([ - ["name", goString], - ["type", goString], - ["value", goString], + shapeStruct([ + ["name", shapeString], + ["type", shapeString], + ["value", shapeString], ]), ], [ "ssl", - goStruct([ - ["status", goString], - ["validation_errors", goPtr(goSlice(goAny))], + shapeStruct([ + ["status", shapeString], + ["validation_errors", shapePtr(shapeSlice(shapeAny))], [ "validation_records", - goSlice( - goStruct([ - ["txt_name", goString], - ["txt_value", goString], + shapeSlice( + shapeStruct([ + ["txt_name", shapeString], + ["txt_value", shapeString], ]), ), ], ]), ], - ["status", goString], - ["verification_errors", goPtr(goSlice(goString))], + ["status", shapeString], + ["verification_errors", shapePtr(shapeSlice(shapeString))], ]), ], - ["success", goBool], + ["success", shapeBool], ]), ], - ["status", goString], + ["status", shapeString], ]); const payload = { custom_hostname: "custom.example.com", @@ -198,7 +192,7 @@ WithData = true }, }, }; - expect(encodeGoToml(payload, spec)).toBe( + expect(encodeStructToml(payload, spec)).toBe( `CustomHostname = "custom.example.com" Status = "2_initiated" @@ -223,35 +217,35 @@ Status = "2_initiated" ); }); - it("escapes strings like BurntSushi and quotes string-typed timestamps (sso provider)", () => { - const spec = goStruct([ - ["created_at", goPtr(goString)], + it("escapes TOML strings and quotes string-typed timestamps (sso provider)", () => { + const spec = shapeStruct([ + ["created_at", shapePtr(shapeString)], [ "domains", - goPtr( - goSlice( - goStruct([ - ["created_at", goPtr(goString)], - ["domain", goPtr(goString)], - ["updated_at", goPtr(goString)], + shapePtr( + shapeSlice( + shapeStruct([ + ["created_at", shapePtr(shapeString)], + ["domain", shapePtr(shapeString)], + ["updated_at", shapePtr(shapeString)], ]), ), ), ], - ["id", goString], + ["id", shapeString], [ "saml", - goPtr( - goStruct([ - ["attribute_mapping", goPtr(goStruct([["keys", goMap(goAny)]]))], - ["entity_id", goString], - ["metadata_url", goPtr(goString)], - ["metadata_xml", goPtr(goString)], - ["name_id_format", goPtr(goString)], + shapePtr( + shapeStruct([ + ["attribute_mapping", shapePtr(shapeStruct([["keys", shapeMap(shapeAny)]]))], + ["entity_id", shapeString], + ["metadata_url", shapePtr(shapeString)], + ["metadata_xml", shapePtr(shapeString)], + ["name_id_format", shapePtr(shapeString)], ]), ), ], - ["updated_at", goPtr(goString)], + ["updated_at", shapePtr(shapeString)], ]); const payload = { id: "8b64a95d-6e29-4c58-8f04-1d0ac6bcda31", @@ -264,7 +258,7 @@ Status = "2_initiated" '\n&', }, }; - expect(encodeGoToml(payload, spec)).toBe( + expect(encodeStructToml(payload, spec)).toBe( `CreatedAt = "2026-05-27T01:02:03.123456Z" Id = "8b64a95d-6e29-4c58-8f04-1d0ac6bcda31" UpdatedAt = "2026-05-27T01:02:03.123456Z" @@ -280,17 +274,17 @@ UpdatedAt = "2026-05-27T01:02:03.123456Z" ); }); - it("keeps hand-written Go struct declaration order (services imageVersion)", () => { - const spec = goTomlListWrapper( + it("keeps hand-written struct declaration order (services imageVersion)", () => { + const spec = shapeTomlListWrapper( "services", - goStruct([ - ["name", goString], - ["local", goString], - ["remote", goString], + shapeStruct([ + ["name", shapeString], + ["local", shapeString], + ["remote", shapeString], ]), ); expect( - encodeGoToml( + encodeStructToml( { services: [{ name: "supabase/postgres", local: "17.4.1.037", remote: "" }] }, spec, ), @@ -304,33 +298,33 @@ UpdatedAt = "2026-05-27T01:02:03.123456Z" }); it("renders inline primitive arrays (network bans wrapper)", () => { - const spec = goStruct([["banned_ips", goSlice(goString), "banned_ips"]]); - expect(encodeGoToml({ banned_ips: ["1.2.3.4", "5.6.7.8"] }, spec)).toBe( + const spec = shapeStruct([["banned_ips", shapeSlice(shapeString), "banned_ips"]]); + expect(encodeStructToml({ banned_ips: ["1.2.3.4", "5.6.7.8"] }, spec)).toBe( 'banned_ips = ["1.2.3.4", "5.6.7.8"]\n', ); }); - it("skips nil nullable fields and fails like Go on populated ones", () => { - const spec = goStruct([ - ["desc", goNullable(goString)], - ["name", goString], + it("skips nil nullable fields and fails on populated ones", () => { + const spec = shapeStruct([ + ["desc", shapeNullable(shapeString)], + ["name", shapeString], ]); - expect(encodeGoToml({ name: "x" }, spec)).toBe('Name = "x"\n'); - expect(() => encodeGoToml({ name: "x", desc: null }, spec)).toThrow( - new GoTomlEncodeError().message, + expect(encodeStructToml({ name: "x" }, spec)).toBe('Name = "x"\n'); + expect(() => encodeStructToml({ name: "x", desc: null }, spec)).toThrow( + new TomlEncodeError().message, ); - expect(() => encodeGoToml({ name: "x", desc: "d" }, spec)).toThrow( - "toml: cannot encode a map with non-string key type", + expect(() => encodeStructToml({ name: "x", desc: "d" }, spec)).toThrow( + "cannot encode a map with non-string keys", ); }); - it("renders floats with a decimal point and Go's exponent form", () => { - const spec = goStruct([ - ["f1", goFloat32], - ["f2", goFloat64], - ["f6", goFloat64], + it("renders floats with a decimal point and the exponent form", () => { + const spec = shapeStruct([ + ["f1", shapeFloat32], + ["f2", shapeFloat64], + ["f6", shapeFloat64], ]); - expect(encodeGoToml({ f1: 1, f2: 1000000, f6: 1234567 }, spec)).toBe( + expect(encodeStructToml({ f1: 1, f2: 1000000, f6: 1234567 }, spec)).toBe( `F1 = 1.0 F2 = 1e+06 F6 = 1.234567e+06 @@ -339,9 +333,9 @@ F6 = 1.234567e+06 }); it("sorts map keys and quotes non-bare keys (branches get envs)", () => { - const spec = goMap(goString); + const spec = shapeMap(shapeString); expect( - encodeGoToml( + encodeStructToml( { SUPABASE_ANON_KEY: "anon", POSTGRES_URL: "postgres://u:p@h:6543/postgres" }, spec, ), @@ -352,95 +346,95 @@ SUPABASE_ANON_KEY = "anon" ); }); - it("renders map elements of mixed interface{} arrays as inline tables like BurntSushi", () => { - const spec = goStruct([["default", goAny, "Default"]]); + it("renders map elements of mixed untyped arrays as inline tables", () => { + const spec = shapeStruct([["default", shapeAny, "Default"]]); // Sorted byte order, non-table values before table values. - expect(encodeGoToml({ default: [{ b: 2, a: 1, C: 3 }, "x"] }, spec)).toBe( + expect(encodeStructToml({ default: [{ b: 2, a: 1, C: 3 }, "x"] }, spec)).toBe( 'Default = [{C = 3.0, a = 1.0, b = 2.0}, "x"]\n', ); - expect(encodeGoToml({ default: [{ a: { b: 1 }, z: 2 }, "x"] }, spec)).toBe( + expect(encodeStructToml({ default: [{ a: { b: 1 }, z: 2 }, "x"] }, spec)).toBe( 'Default = [{z = 2.0, a = {b = 1.0}}, "x"]\n', ); - expect(encodeGoToml({ default: [{ a: [{ b: 1 }], z: 2 }, "x"] }, spec)).toBe( + expect(encodeStructToml({ default: [{ a: [{ b: 1 }], z: 2 }, "x"] }, spec)).toBe( 'Default = [{z = 2.0, a = [{b = 1.0}]}, "x"]\n', ); // Non-bare keys are quoted; empty and all-nil tables collapse to {}. - expect(encodeGoToml({ default: [{ "a b": 1 }, "x"] }, spec)).toBe( + expect(encodeStructToml({ default: [{ "a b": 1 }, "x"] }, spec)).toBe( 'Default = [{"a b" = 1.0}, "x"]\n', ); - expect(encodeGoToml({ default: [{}, "x"] }, spec)).toBe('Default = [{}, "x"]\n'); - expect(encodeGoToml({ default: [{ a: null }, "x"] }, spec)).toBe('Default = [{}, "x"]\n'); + expect(encodeStructToml({ default: [{}, "x"] }, spec)).toBe('Default = [{}, "x"]\n'); + expect(encodeStructToml({ default: [{ a: null }, "x"] }, spec)).toBe('Default = [{}, "x"]\n'); // eMap decides the ", " separator by group position before skipping nil // entries, so a nil in the final position leaves a dangling separator. - expect(encodeGoToml({ default: [{ "10": 78797, b: null }, false] }, spec)).toBe( + expect(encodeStructToml({ default: [{ "10": 78797, b: null }, false] }, spec)).toBe( "Default = [{10 = 78797.0, }, false]\n", ); }); - it("fails like Go on nil elements inside interface{} arrays", () => { - const spec = goStruct([["default", goAny, "Default"]]); - const message = "toml: cannot encode array with nil element"; - expect(() => encodeGoToml({ default: [null, "x"] }, spec)).toThrow(message); - expect(() => encodeGoToml({ default: [null] }, spec)).toThrow(message); - expect(() => encodeGoToml({ default: [[null], "x"] }, spec)).toThrow(message); + it("fails on null elements inside untyped arrays", () => { + const spec = shapeStruct([["default", shapeAny, "Default"]]); + const message = "cannot encode an array with a null element"; + expect(() => encodeStructToml({ default: [null, "x"] }, spec)).toThrow(message); + expect(() => encodeStructToml({ default: [null] }, spec)).toThrow(message); + expect(() => encodeStructToml({ default: [[null], "x"] }, spec)).toThrow(message); }); - it("truncates time fractions to nanoseconds like time.Time's decoder", () => { - const spec = goStruct([["t", goTime, "T"]]); - expect(encodeGoToml({ t: "2026-01-01T00:00:00.1234567895Z" }, spec)).toBe( + it("truncates time fractions to nanoseconds", () => { + const spec = shapeStruct([["t", shapeTime, "T"]]); + expect(encodeStructToml({ t: "2026-01-01T00:00:00.1234567895Z" }, spec)).toBe( "T = 2026-01-01T00:00:00.123456789Z\n", ); - expect(encodeGoToml({ t: "2026-01-01T00:00:00.1000000005Z" }, spec)).toBe( + expect(encodeStructToml({ t: "2026-01-01T00:00:00.1000000005Z" }, spec)).toBe( "T = 2026-01-01T00:00:00.1Z\n", ); }); - it("quotes comma-fraction timestamp-shaped STRINGS like yaml.v3's resolver", () => { - const spec = goStruct([["s", goString, "S"]]); - expect(encodeGoYaml({ s: "2026-01-01T00:00:00,123Z" }, spec)).toBe( + it("quotes comma-fraction timestamp-shaped strings", () => { + const spec = shapeStruct([["s", shapeString, "S"]]); + expect(encodeStructYaml({ s: "2026-01-01T00:00:00,123Z" }, spec)).toBe( 's: "2026-01-01T00:00:00,123Z"\n', ); }); - it("leaves overflowing float-shaped strings plain like yaml.v3's ParseFloat gate", () => { - const spec = goStruct([["s", goString, "S"]]); - expect(encodeGoYaml({ s: "1e999" }, spec)).toBe("s: 1e999\n"); - expect(encodeGoYaml({ s: "-1e999" }, spec)).toBe("s: -1e999\n"); - expect(encodeGoYaml({ s: ".5e999" }, spec)).toBe("s: .5e999\n"); - expect(encodeGoYaml({ s: "1e-999" }, spec)).toBe('s: "1e-999"\n'); - expect(encodeGoYaml({ s: "1e10" }, spec)).toBe('s: "1e10"\n'); + it("leaves overflowing float-shaped strings plain", () => { + const spec = shapeStruct([["s", shapeString, "S"]]); + expect(encodeStructYaml({ s: "1e999" }, spec)).toBe("s: 1e999\n"); + expect(encodeStructYaml({ s: "-1e999" }, spec)).toBe("s: -1e999\n"); + expect(encodeStructYaml({ s: ".5e999" }, spec)).toBe("s: .5e999\n"); + expect(encodeStructYaml({ s: "1e-999" }, spec)).toBe('s: "1e-999"\n'); + expect(encodeStructYaml({ s: "1e10" }, spec)).toBe('s: "1e10"\n'); }); - it("wraps 19+-digit numeric key runs like Go's unchecked int64 accumulation", () => { - const spec = goStruct([["default", goAny, "Default"]]); + it("wraps 19+-digit numeric key runs via unchecked int64 accumulation", () => { + const spec = shapeStruct([["default", shapeAny, "Default"]]); expect( - encodeGoYaml({ default: { a9000000000000000000: 1, a10000000000000000000: 2 } }, spec), + encodeStructYaml({ default: { a9000000000000000000: 1, a10000000000000000000: 2 } }, spec), ).toBe("default:\n a10000000000000000000: 2\n a9000000000000000000: 1\n"); }); - it("orders Unicode-digit map keys with yaml.v3's naive rune arithmetic", () => { - const spec = goStruct([["default", goAny, "Default"]]); - expect(encodeGoYaml({ default: { a٢: 1, a3: 2, a10: 3, a9: 4 } }, spec)).toBe( + it("orders Unicode-digit map keys by code point, with numeric runs compared by ASCII digits only", () => { + const spec = shapeStruct([["default", shapeAny, "Default"]]); + expect(encodeStructYaml({ default: { a٢: 1, a3: 2, a10: 3, a9: 4 } }, spec)).toBe( "default:\n a3: 2\n a9: 4\n a10: 3\n a٢: 1\n", ); }); - it("normalizes Go's accepted comma fractional separator to the dot Go re-emits", () => { - const spec = goStruct([["t", goTime, "T"]]); - expect(encodeGoToml({ t: "2026-01-01T00:00:00,123Z" }, spec)).toBe( + it("normalizes a comma fractional separator to a dot", () => { + const spec = shapeStruct([["t", shapeTime, "T"]]); + expect(encodeStructToml({ t: "2026-01-01T00:00:00,123Z" }, spec)).toBe( "T = 2026-01-01T00:00:00.123Z\n", ); - expect(encodeGoYaml({ t: "2026-01-01T00:00:00,1234567895Z" }, spec)).toBe( + expect(encodeStructYaml({ t: "2026-01-01T00:00:00,1234567895Z" }, spec)).toBe( "t: 2026-01-01T00:00:00.123456789Z\n", ); }); - it("sorts map keys by UTF-8 byte order like Go's sort.Strings", () => { + it("sorts map keys by UTF-8 byte order", () => { // U+E000/U+FF21 sort before the astral U+1D400/U+1F600 in UTF-8 byte order; JS `<` on UTF-16 // units would sort both astral keys first. - const spec = goMap(goString); + const spec = shapeMap(shapeString); expect( - encodeGoToml( + encodeStructToml( { "\u{1F600}": "emoji", "\uE000": "private-use", @@ -463,9 +457,9 @@ SUPABASE_ANON_KEY = "anon" }); }); -describe("encodeGoYaml", () => { - it("matches Go byte-for-byte for a branches list (lowercased keys, explicit nulls)", () => { - expect(encodeGoYaml([SAMPLE_BRANCH, ZERO_BRANCH], goSlice(BRANCH_RESPONSE))).toBe( +describe("encodeStructYaml", () => { + it("matches the golden output for a branches list (lowercased keys, explicit nulls)", () => { + expect(encodeStructYaml([SAMPLE_BRANCH, ZERO_BRANCH], shapeSlice(BRANCH_RESPONSE))).toBe( `- createdat: 2026-05-27T01:02:03Z deletionscheduledat: null gitbranch: feat-1 @@ -505,39 +499,39 @@ describe("encodeGoYaml", () => { }); it("renders an empty list as [] regardless of nil-ness", () => { - expect(encodeGoYaml([], goSlice(BRANCH_RESPONSE))).toBe("[]\n"); - expect(encodeGoYaml(undefined, goSlice(BRANCH_RESPONSE))).toBe("[]\n"); + expect(encodeStructYaml([], shapeSlice(BRANCH_RESPONSE))).toBe("[]\n"); + expect(encodeStructYaml(undefined, shapeSlice(BRANCH_RESPONSE))).toBe("[]\n"); }); it("uses 4-column indentation, block literals, and quoted string timestamps (sso show)", () => { - const spec = goStruct([ - ["created_at", goPtr(goString)], + const spec = shapeStruct([ + ["created_at", shapePtr(shapeString)], [ "domains", - goPtr( - goSlice( - goStruct([ - ["created_at", goPtr(goString)], - ["domain", goPtr(goString)], - ["updated_at", goPtr(goString)], + shapePtr( + shapeSlice( + shapeStruct([ + ["created_at", shapePtr(shapeString)], + ["domain", shapePtr(shapeString)], + ["updated_at", shapePtr(shapeString)], ]), ), ), ], - ["id", goString], + ["id", shapeString], [ "saml", - goPtr( - goStruct([ - ["attribute_mapping", goPtr(goStruct([["keys", goMap(goAny)]]))], - ["entity_id", goString], - ["metadata_url", goPtr(goString)], - ["metadata_xml", goPtr(goString)], - ["name_id_format", goPtr(goString)], + shapePtr( + shapeStruct([ + ["attribute_mapping", shapePtr(shapeStruct([["keys", shapeMap(shapeAny)]]))], + ["entity_id", shapeString], + ["metadata_url", shapePtr(shapeString)], + ["metadata_xml", shapePtr(shapeString)], + ["name_id_format", shapePtr(shapeString)], ]), ), ], - ["updated_at", goPtr(goString)], + ["updated_at", shapePtr(shapeString)], ]); const payload = { id: "8b64a95d-6e29-4c58-8f04-1d0ac6bcda31", @@ -550,7 +544,7 @@ describe("encodeGoYaml", () => { '\n&', }, }; - expect(encodeGoYaml(payload, spec)).toBe( + expect(encodeStructYaml(payload, spec)).toBe( `createdat: "2026-05-27T01:02:03.123456Z" domains: - createdat: "2026-05-27T01:02:03Z" @@ -570,27 +564,27 @@ updatedat: "2026-05-27T01:02:03.123456Z" ); }); - it("renders nullable fields the way yaml.v3 renders map[bool]T (api keys)", () => { + it("renders nullable fields as a mapping with a plain bool key (api keys)", () => { // Mirror of api.ApiKeyResponse. - const spec = goSlice( - goStruct([ - ["api_key", goNullable(goString)], - ["description", goNullable(goString)], - ["hash", goNullable(goString)], - ["id", goNullable(goString)], - ["inserted_at", goNullable(goTime)], - ["name", goString], - ["prefix", goNullable(goString)], - ["secret_jwt_template", goNullable(goMap(goAny))], - ["type", goNullable(goString)], - ["updated_at", goNullable(goTime)], + const spec = shapeSlice( + shapeStruct([ + ["api_key", shapeNullable(shapeString)], + ["description", shapeNullable(shapeString)], + ["hash", shapeNullable(shapeString)], + ["id", shapeNullable(shapeString)], + ["inserted_at", shapeNullable(shapeTime)], + ["name", shapeString], + ["prefix", shapeNullable(shapeString)], + ["secret_jwt_template", shapeNullable(shapeMap(shapeAny))], + ["type", shapeNullable(shapeString)], + ["updated_at", shapeNullable(shapeTime)], ]), ); const payload = [ { name: "anon", api_key: "anon-key-value", id: "key-id-1", type: "legacy" }, { name: "service_role" }, ]; - expect(encodeGoYaml(payload, spec)).toBe( + expect(encodeStructYaml(payload, spec)).toBe( `- apikey: true: anon-key-value description: {} @@ -619,11 +613,11 @@ updatedat: "2026-05-27T01:02:03.123456Z" }); it("renders an explicit JSON null nullable as a false-keyed zero (snippets description)", () => { - const spec = goStruct([ - ["desc", goNullable(goString)], - ["name", goString], + const spec = shapeStruct([ + ["desc", shapeNullable(shapeString)], + ["name", shapeString], ]); - expect(encodeGoYaml({ desc: null, name: "x" }, spec)).toBe( + expect(encodeStructYaml({ desc: null, name: "x" }, spec)).toBe( `desc: false: "" name: x @@ -632,28 +626,28 @@ name: x }); it("renders nil and empty slices as [] and nested maps at +4 (backups list)", () => { - const spec = goStruct([ + const spec = shapeStruct([ [ "backups", - goSlice( - goStruct([ - ["id", goInt], - ["inserted_at", goString], - ["is_physical_backup", goBool], - ["status", goString], + shapeSlice( + shapeStruct([ + ["id", shapeInt], + ["inserted_at", shapeString], + ["is_physical_backup", shapeBool], + ["status", shapeString], ]), ), ], [ "physical_backup_data", - goStruct([ - ["earliest_physical_backup_date_unix", goPtr(goInt)], - ["latest_physical_backup_date_unix", goPtr(goInt)], + shapeStruct([ + ["earliest_physical_backup_date_unix", shapePtr(shapeInt)], + ["latest_physical_backup_date_unix", shapePtr(shapeInt)], ]), ], - ["pitr_enabled", goBool], - ["region", goString], - ["walg_enabled", goBool], + ["pitr_enabled", shapeBool], + ["region", shapeString], + ["walg_enabled", shapeBool], ]); const payload = { backups: [], @@ -662,7 +656,7 @@ name: x region: "us-east-1", walg_enabled: true, }; - expect(encodeGoYaml(payload, spec)).toBe( + expect(encodeStructYaml(payload, spec)).toBe( `backups: [] physicalbackupdata: earliestphysicalbackupdateunix: 1687279254 @@ -674,8 +668,8 @@ walgenabled: true ); }); - it("quotes strings exactly like yaml.v3's resolver and emitter", () => { - const spec = goMap(goString); + it("quotes strings that would resolve to a non-string YAML tag", () => { + const spec = shapeMap(shapeString); const payload = { k01: "yes", k04: "~", @@ -708,7 +702,7 @@ walgenabled: true k52: "2001-12-14 21:59:43.10 -5", k53: "2001-12-15 2:59:43.10", }; - expect(encodeGoYaml(payload, spec)).toBe( + expect(encodeStructYaml(payload, spec)).toBe( `k01: "yes" k04: "~" k07: ' leading-space' @@ -743,10 +737,13 @@ k53: "2001-12-15 2:59:43.10" ); }); - it("renders block literal chomping indicators like yaml.v3", () => { - const spec = goMap(goString); + it("renders block literal chomping indicators", () => { + const spec = shapeMap(shapeString); expect( - encodeGoYaml({ k27: "line1\nline2\n", k28: "line1\nline2\n\n", k29: "with\rcarriage" }, spec), + encodeStructYaml( + { k27: "line1\nline2\n", k28: "line1\nline2\n\n", k29: "with\rcarriage" }, + spec, + ), ).toBe( `k27: | line1 @@ -760,9 +757,11 @@ k29: "with\\rcarriage" ); }); - it("renders floats with Go's g-format exponent switch", () => { - const spec = goMap(goAny); - expect(encodeGoYaml({ f2: 1000000, f3: 78125, f4: 0.5, f5: 0.000001, f6: 1234567 }, spec)).toBe( + it("renders floats with the g-format exponent switch", () => { + const spec = shapeMap(shapeAny); + expect( + encodeStructYaml({ f2: 1000000, f3: 78125, f4: 0.5, f5: 0.000001, f6: 1234567 }, spec), + ).toBe( `f2: 1e+06 f3: 78125 f4: 0.5 @@ -772,9 +771,9 @@ f6: 1.234567e+06 ); }); - it("sorts plain map keys with yaml.v3's natural ordering", () => { - const spec = goMap(goString); - expect(encodeGoYaml({ z: "1", a: "2", "10": "3", "2": "4", B: "5", b: "6" }, spec)).toBe( + it("sorts plain map keys in natural order", () => { + const spec = shapeMap(shapeString); + expect(encodeStructYaml({ z: "1", a: "2", "10": "3", "2": "4", B: "5", b: "6" }, spec)).toBe( `"2": "4" "10": "3" B: "5" @@ -785,12 +784,12 @@ z: "1" ); }); - it("sorts unicode map keys by rune and escapes astral keys like yaml.v3", () => { - // keyList.Less compares runes, so the astral U+1F600/U+1D400 sort after U+E000/U+FF21; the - // emitter double-quotes astral characters as \U-escapes (not printable to libyaml). - const spec = goMap(goString); + it("sorts unicode map keys by code point and double-quotes astral keys with \\U escapes", () => { + // Keys compare by code point, so the astral U+1F600/U+1D400 sort after U+E000/U+FF21; the + // emitter double-quotes astral characters as \U-escapes (not printable in YAML). + const spec = shapeMap(shapeString); expect( - encodeGoYaml( + encodeStructYaml( { "\u{1F600}": "emoji", "\uE000": "private-use", @@ -813,9 +812,9 @@ z: ascii }); it("validates calendar dates and zone offsets like time.Parse before quoting timestamps", () => { - const spec = goMap(goString); + const spec = shapeMap(shapeString); expect( - encodeGoYaml( + encodeStructYaml( { t01: "2025-02-31", t02: "2024-02-29", @@ -849,29 +848,29 @@ t12: 1900-02-29 ); }); - it("truncates time fractions to nanoseconds like time.Time's decoder", () => { - const spec = goStruct([["t", goTime, "T"]]); - expect(encodeGoYaml({ t: "2026-01-01T00:00:00.1234567895Z" }, spec)).toBe( + it("truncates time fractions to nanoseconds", () => { + const spec = shapeStruct([["t", shapeTime, "T"]]); + expect(encodeStructYaml({ t: "2026-01-01T00:00:00.1234567895Z" }, spec)).toBe( "t: 2026-01-01T00:00:00.123456789Z\n", ); - expect(encodeGoYaml({ t: "2026-01-01T00:00:00.12345678901234Z" }, spec)).toBe( + expect(encodeStructYaml({ t: "2026-01-01T00:00:00.12345678901234Z" }, spec)).toBe( "t: 2026-01-01T00:00:00.123456789Z\n", ); - expect(encodeGoYaml({ t: "2026-01-01T00:00:00.9999999999Z" }, spec)).toBe( + expect(encodeStructYaml({ t: "2026-01-01T00:00:00.9999999999Z" }, spec)).toBe( "t: 2026-01-01T00:00:00.999999999Z\n", ); - expect(encodeGoYaml({ t: "2026-01-01T00:00:00.1000000005Z" }, spec)).toBe( + expect(encodeStructYaml({ t: "2026-01-01T00:00:00.1000000005Z" }, spec)).toBe( "t: 2026-01-01T00:00:00.1Z\n", ); - expect(encodeGoYaml({ t: "2026-01-01T00:00:00.1234567895+07:00" }, spec)).toBe( + expect(encodeStructYaml({ t: "2026-01-01T00:00:00.1234567895+07:00" }, spec)).toBe( "t: 2026-01-01T00:00:00.123456789+07:00\n", ); }); - it("escapes non-printable scalars with \\x/\\u/\\U like yaml.v3's emitter", () => { - const spec = goMap(goString); + it("escapes non-printable scalars with \\x/\\u/\\U", () => { + const spec = shapeMap(shapeString); expect( - encodeGoYaml( + encodeStructYaml( { e1: "a\uFEFFb", e2: "a\uFFFEb", @@ -892,50 +891,50 @@ e5: "nel\\Nbreak" }); }); -describe("goFieldName", () => { +describe("pascalCaseFieldName", () => { it("capitalizes snake_case tokens like oapi-codegen", () => { - expect(goFieldName("api_key")).toBe("ApiKey"); - expect(goFieldName("metadata_xml")).toBe("MetadataXml"); - expect(goFieldName("parent_project_ref")).toBe("ParentProjectRef"); - expect(goFieldName("ezbr_sha256")).toBe("EzbrSha256"); + expect(pascalCaseFieldName("api_key")).toBe("ApiKey"); + expect(pascalCaseFieldName("metadata_xml")).toBe("MetadataXml"); + expect(pascalCaseFieldName("parent_project_ref")).toBe("ParentProjectRef"); + expect(pascalCaseFieldName("ezbr_sha256")).toBe("EzbrSha256"); }); it("capitalizes the first letter of camelCase tags", () => { - expect(goFieldName("appliedSuccessfully")).toBe("AppliedSuccessfully"); - expect(goFieldName("currentConfig")).toBe("CurrentConfig"); + expect(pascalCaseFieldName("appliedSuccessfully")).toBe("AppliedSuccessfully"); + expect(pascalCaseFieldName("currentConfig")).toBe("CurrentConfig"); }); }); -describe("goFormatFloat", () => { - it("matches strconv.FormatFloat(f, 'g', -1, 64)", () => { - expect(goFormatFloat(1, 64)).toBe("1"); - expect(goFormatFloat(123456, 64)).toBe("123456"); - expect(goFormatFloat(1000000, 64)).toBe("1e+06"); - expect(goFormatFloat(1234567, 64)).toBe("1.234567e+06"); - expect(goFormatFloat(0.5, 64)).toBe("0.5"); - expect(goFormatFloat(0.000001, 64)).toBe("1e-06"); - expect(goFormatFloat(0, 64)).toBe("0"); - expect(goFormatFloat(-2.5, 64)).toBe("-2.5"); +describe("formatStructFloat", () => { + it("formats shortest round-trip digits for 64-bit floats", () => { + expect(formatStructFloat(1, 64)).toBe("1"); + expect(formatStructFloat(123456, 64)).toBe("123456"); + expect(formatStructFloat(1000000, 64)).toBe("1e+06"); + expect(formatStructFloat(1234567, 64)).toBe("1.234567e+06"); + expect(formatStructFloat(0.5, 64)).toBe("0.5"); + expect(formatStructFloat(0.000001, 64)).toBe("1e-06"); + expect(formatStructFloat(0, 64)).toBe("0"); + expect(formatStructFloat(-2.5, 64)).toBe("-2.5"); }); - it("rounds through float32 like Go's typed fields", () => { - expect(goFormatFloat(16777217, 32)).toBe("1.6777216e+07"); - expect(goFormatFloat(78125, 32)).toBe("78125"); - expect(goFormatFloat(0.5, 32)).toBe("0.5"); + it("rounds through float32 for typed fields", () => { + expect(formatStructFloat(16777217, 32)).toBe("1.6777216e+07"); + expect(formatStructFloat(78125, 32)).toBe("78125"); + expect(formatStructFloat(0.5, 32)).toBe("0.5"); }); it("breaks exact shortest-digit ties to even like Ryu, not half-up", () => { // 4249.03125 sits exactly between the two shortest 8-digit candidates, rounding to the even // final digit both ways. - expect(goFormatFloat(4249.03125, 32)).toBe("4249.0312"); - expect(goFormatFloat(4249.09375, 32)).toBe("4249.0938"); - expect(goFormatFloat(123456789, 32)).toBe("1.2345679e+08"); - expect(goFormatFloat(1048575.5, 32)).toBe("1.0485755e+06"); - expect(goFormatFloat(8388607.5, 32)).toBe("8.3886075e+06"); + expect(formatStructFloat(4249.03125, 32)).toBe("4249.0312"); + expect(formatStructFloat(4249.09375, 32)).toBe("4249.0938"); + expect(formatStructFloat(123456789, 32)).toBe("1.2345679e+08"); + expect(formatStructFloat(1048575.5, 32)).toBe("1.0485755e+06"); + expect(formatStructFloat(8388607.5, 32)).toBe("8.3886075e+06"); // Boundaries: smallest subnormal, subnormal→normal edge, and max finite. - expect(goFormatFloat(1.401298464324817e-45, 32)).toBe("1e-45"); - expect(goFormatFloat(1.1754943508222875e-38, 32)).toBe("1.1754944e-38"); - expect(goFormatFloat(3.4028234663852886e38, 32)).toBe("3.4028235e+38"); - expect(goFormatFloat(-4249.03125, 32)).toBe("-4249.0312"); + expect(formatStructFloat(1.401298464324817e-45, 32)).toBe("1e-45"); + expect(formatStructFloat(1.1754943508222875e-38, 32)).toBe("1.1754944e-38"); + expect(formatStructFloat(3.4028234663852886e38, 32)).toBe("3.4028235e+38"); + expect(formatStructFloat(-4249.03125, 32)).toBe("-4249.0312"); }); }); diff --git a/apps/cli/src/command-internal/go-struct-output.openapi-drift.unit.test.ts b/apps/cli/src/command-internal/struct-output.openapi-drift.unit.test.ts similarity index 77% rename from apps/cli/src/command-internal/go-struct-output.openapi-drift.unit.test.ts rename to apps/cli/src/command-internal/struct-output.openapi-drift.unit.test.ts index 23ec1da4e8..78bccd0c81 100644 --- a/apps/cli/src/command-internal/go-struct-output.openapi-drift.unit.test.ts +++ b/apps/cli/src/command-internal/struct-output.openapi-drift.unit.test.ts @@ -1,23 +1,23 @@ import { describe, expect, it } from "vitest"; import openApiSpec from "@supabase/api/openapi.json"; -import { GO_BRANCH_RESPONSE } from "../commands/branches/branches.go-payload.ts"; -import { GO_ORGANIZATION_RESPONSE } from "../commands/orgs/orgs.go-payload.ts"; -import { GO_SSL_ENFORCEMENT_RESPONSE } from "../commands/ssl-enforcement/ssl-enforcement.go-payload.ts"; -import { GO_SSO_PROVIDER_RESPONSE } from "../commands/sso/sso.go-payload.ts"; -import type { GoType } from "./go-struct-output.encoders.ts"; +import { BRANCH_RESPONSE_SHAPE } from "../commands/branches/branches.response-shape.ts"; +import { ORGANIZATION_RESPONSE_SHAPE } from "../commands/orgs/orgs.response-shape.ts"; +import { SSL_ENFORCEMENT_RESPONSE_SHAPE } from "../commands/ssl-enforcement/ssl-enforcement.response-shape.ts"; +import { SSO_PROVIDER_RESPONSE_SHAPE } from "../commands/sso/sso.response-shape.ts"; +import type { OutputShape } from "./struct-output.encoders.ts"; /** - * Mechanical drift check for the `*.go-payload.ts` specs against the live OpenAPI schemas they - * mirror — field-name set, `goPtr` vs `required`, field order, and value type/kind must all + * Mechanical drift check for the `*.response-shape.ts` specs against the live OpenAPI schemas they + * mirror — field-name set, `shapePtr` vs `required`, field order, and value type/kind must all * match, or a future spec edit silently changes `-o yaml`/`-o toml` bytes instead of failing a * test. * - * oapi-codegen names its Go struct fields from the schema's JSON keys and declares them in - * alphabetical order, not the schema's own `properties` order, so field order here is compared - * against an ASCII sort of `properties` keys. That assumption holds for every schema below; - * `KNOWN_ORDER_EXCEPTIONS` records the real order for any future schema where oapi-codegen's - * Go-identifier sort diverges from a plain JSON-key sort. + * Struct fields are named from the schema's JSON keys and declared in alphabetical order, not + * the schema's own `properties` order, so field order here is compared against an ASCII sort of + * `properties` keys. That assumption holds for every schema below; `KNOWN_ORDER_EXCEPTIONS` + * records the real order for any future schema where the field-name sort diverges from a plain + * JSON-key sort. */ interface JsonSchema { @@ -51,23 +51,23 @@ function resolveSchema(schema: JsonSchema): JsonSchema { return resolved; } -interface GoStructField { +interface OutputShapeField { readonly json: string; - readonly type: GoType; + readonly type: OutputShape; } -function structFieldsOf(spec: GoType): ReadonlyArray { +function structFieldsOf(spec: OutputShape): ReadonlyArray { if (spec.kind !== "struct") { - throw new Error(`expected a struct GoType, got "${spec.kind}"`); + throw new Error(`expected a struct OutputShape, got "${spec.kind}"`); } return spec.fields; } -function isPointerType(type: GoType): boolean { +function isPointerType(type: OutputShape): boolean { return type.kind === "ptr" || type.kind === "nullable"; } -function unwrapPointer(type: GoType): GoType { +function unwrapPointer(type: OutputShape): OutputShape { return type.kind === "ptr" || type.kind === "nullable" ? type.elem : type; } @@ -79,7 +79,7 @@ interface DriftMismatch { } function compareFieldSet( - fields: ReadonlyArray, + fields: ReadonlyArray, schema: JsonSchema, path: string, ): ReadonlyArray { @@ -108,15 +108,15 @@ function compareFieldSet( } function comparePointerRequired( - fields: ReadonlyArray, + fields: ReadonlyArray, schema: JsonSchema, path: string, ): ReadonlyArray { const required = new Set(schema.required ?? []); const mismatches: Array = []; for (const field of fields) { - // oapi-codegen never pointer-wraps a Go map regardless of the schema's `required` list, so - // this is the one field kind the goPtr/required rule doesn't hold for. + // A map field is never pointer-wrapped regardless of the schema's `required` list, so + // this is the one field kind the shapePtr/required rule doesn't hold for. if (unwrapPointer(field.type).kind === "map") { continue; } @@ -126,8 +126,8 @@ function comparePointerRequired( mismatches.push({ path: `${path}.${field.json}`, message: isPointer - ? `spec marks "${field.json}" as goPtr, but the schema marks it required` - : `spec does not mark "${field.json}" as goPtr, but the schema marks it optional`, + ? `spec marks "${field.json}" as shapePtr, but the schema marks it required` + : `spec does not mark "${field.json}" as shapePtr, but the schema marks it optional`, }); } } @@ -135,7 +135,7 @@ function comparePointerRequired( } function compareValueType( - type: GoType, + type: OutputShape, schema: JsonSchema, path: string, ): ReadonlyArray { @@ -162,7 +162,7 @@ function compareValueType( return [ { path, - message: `expected type=string format=date-time (Go time.Time), got type=${resolved.type ?? ""} format=${resolved.format ?? ""}`, + message: `expected type=string format=date-time (timestamp), got type=${resolved.type ?? ""} format=${resolved.format ?? ""}`, }, ]; } @@ -232,7 +232,7 @@ function compareValueType( } function compareValueTypes( - fields: ReadonlyArray, + fields: ReadonlyArray, schema: JsonSchema, path: string, ): ReadonlyArray { @@ -248,7 +248,7 @@ function compareValueTypes( } function compareFieldOrder( - fields: ReadonlyArray, + fields: ReadonlyArray, schema: JsonSchema, schemaKey: string, path: string, @@ -269,13 +269,13 @@ function compareFieldOrder( } /** - * Walks a {@link GoType} struct spec and the corresponding OpenAPI schema in lockstep, returning + * Walks a {@link OutputShape} struct spec and the corresponding OpenAPI schema in lockstep, returning * every mismatch found. Recurses into a struct field, and into a slice field whose element is a * struct, so both `saml`'s nested fields and `domains`' array elements get the same field-set, * pointer/required, order, and value-type checks as the top-level struct. */ -function compareGoStructToSchema( - spec: GoType, +function compareShapeToSchema( + spec: OutputShape, schema: JsonSchema, schemaName: string, path: string, @@ -298,7 +298,7 @@ function compareGoStructToSchema( } if (inner.kind === "struct") { mismatches.push( - ...compareGoStructToSchema(inner, nestedSchema, schemaName, `${path}.${field.json}`), + ...compareShapeToSchema(inner, nestedSchema, schemaName, `${path}.${field.json}`), ); continue; } @@ -312,7 +312,7 @@ function compareGoStructToSchema( continue; } mismatches.push( - ...compareGoStructToSchema(elem, itemsSchema, schemaName, `${path}.${field.json}[]`), + ...compareShapeToSchema(elem, itemsSchema, schemaName, `${path}.${field.json}[]`), ); } } @@ -320,40 +320,44 @@ function compareGoStructToSchema( return mismatches; } -interface GoPayloadSpecEntry { +interface PayloadShapeEntry { readonly specName: string; - readonly spec: GoType; + readonly spec: OutputShape; readonly schemaName: string; } -const GO_PAYLOAD_SPEC_REGISTRY: ReadonlyArray = [ - { specName: "GO_BRANCH_RESPONSE", spec: GO_BRANCH_RESPONSE, schemaName: "BranchResponse_Output" }, +const PAYLOAD_SHAPE_REGISTRY: ReadonlyArray = [ { - specName: "GO_ORGANIZATION_RESPONSE", - spec: GO_ORGANIZATION_RESPONSE, + specName: "BRANCH_RESPONSE_SHAPE", + spec: BRANCH_RESPONSE_SHAPE, + schemaName: "BranchResponse_Output", + }, + { + specName: "ORGANIZATION_RESPONSE_SHAPE", + spec: ORGANIZATION_RESPONSE_SHAPE, schemaName: "OrganizationResponseV1_Output", }, { - specName: "GO_SSL_ENFORCEMENT_RESPONSE", - spec: GO_SSL_ENFORCEMENT_RESPONSE, + specName: "SSL_ENFORCEMENT_RESPONSE_SHAPE", + spec: SSL_ENFORCEMENT_RESPONSE_SHAPE, schemaName: "SslEnforcementResponse_Output", }, { - specName: "GO_SSO_PROVIDER_RESPONSE", - spec: GO_SSO_PROVIDER_RESPONSE, + specName: "SSO_PROVIDER_RESPONSE_SHAPE", + spec: SSO_PROVIDER_RESPONSE_SHAPE, schemaName: "GetProviderResponse_Output", }, ]; -describe("go-payload specs vs the OpenAPI schema (drift check)", () => { - it.each(GO_PAYLOAD_SPEC_REGISTRY)( +describe("response-shape specs vs the OpenAPI schema (drift check)", () => { + it.each(PAYLOAD_SHAPE_REGISTRY)( "$specName matches the $schemaName schema with zero drift", ({ spec, schemaName }) => { const schema = SCHEMAS[schemaName]; if (schema === undefined) { throw new Error(`missing OpenAPI schema "${schemaName}"`); } - expect(compareGoStructToSchema(spec, schema, schemaName, "$")).toEqual([]); + expect(compareShapeToSchema(spec, schema, schemaName, "$")).toEqual([]); }, ); @@ -368,8 +372,8 @@ describe("go-payload specs vs the OpenAPI schema (drift check)", () => { }, required: ["appliedSuccessfully", "currentConfig"], }; - const mismatches = compareGoStructToSchema( - GO_SSL_ENFORCEMENT_RESPONSE, + const mismatches = compareShapeToSchema( + SSL_ENFORCEMENT_RESPONSE_SHAPE, mutatedSchema, "SslEnforcementResponse", "$", @@ -382,7 +386,7 @@ describe("go-payload specs vs the OpenAPI schema (drift check)", () => { it("has teeth: reports a mismatch when a field's required-ness flips", () => { // A hand-mutated copy of the real BranchResponse schema with `git_branch` moved into - // `required`, though the spec still marks it `goPtr`. + // `required`, though the spec still marks it `shapePtr`. const mutatedSchema: JsonSchema = { type: "object", properties: { @@ -418,8 +422,8 @@ describe("go-payload specs vs the OpenAPI schema (drift check)", () => { "with_data", ], }; - const mismatches = compareGoStructToSchema( - GO_BRANCH_RESPONSE, + const mismatches = compareShapeToSchema( + BRANCH_RESPONSE_SHAPE, mutatedSchema, "BranchResponse", "$", @@ -427,7 +431,7 @@ describe("go-payload specs vs the OpenAPI schema (drift check)", () => { expect(mismatches).toContainEqual( expect.objectContaining({ path: "$.git_branch", - message: expect.stringContaining("goPtr"), + message: expect.stringContaining("shapePtr"), }), ); }); @@ -447,8 +451,8 @@ describe("go-payload specs vs the OpenAPI schema (drift check)", () => { }, required: ["appliedSuccessfully", "currentConfig"], }; - const mismatches = compareGoStructToSchema( - GO_SSL_ENFORCEMENT_RESPONSE, + const mismatches = compareShapeToSchema( + SSL_ENFORCEMENT_RESPONSE_SHAPE, mutatedSchema, "SslEnforcementResponse", "$", diff --git a/apps/cli/src/command-internal/viper-env.ts b/apps/cli/src/command-internal/supabase-env.ts similarity index 52% rename from apps/cli/src/command-internal/viper-env.ts rename to apps/cli/src/command-internal/supabase-env.ts index 96588f2866..477c35de5d 100644 --- a/apps/cli/src/command-internal/viper-env.ts +++ b/apps/cli/src/command-internal/supabase-env.ts @@ -6,23 +6,19 @@ * so callers OR the parsed flag value with this read (flag-set wins). */ -import { Config, Effect, Option } from "effect"; +import { Effect } from "effect"; -const VIPER_TRUE = new Set(["1", "t", "T", "TRUE", "true", "True"]); +import { envValue } from "../shared/config/env-option.ts"; + +const TRUE_VALUES = new Set(["1", "t", "T", "TRUE", "true", "True"]); /** Truthiness for an already-resolved env value (see module doc). */ -function viperBool(raw: string | undefined): boolean { - return raw !== undefined && VIPER_TRUE.has(raw); +export function parseEnvBool(raw: string | undefined): boolean { + return raw !== undefined && TRUE_VALUES.has(raw); } -/** Env-record providers never fail this read, so a `ConfigError` here is a defect. */ -export const shellEnv = (name: string) => - Config.option(Config.String(name)).pipe(Effect.map(Option.getOrUndefined), Effect.orDie); - -/** Reads a single `SUPABASE_*` boolean env var through `Config` (see module doc). */ -export const viperEnvBool = Effect.fnUntraced(function* (name: string) { - return viperBool(yield* shellEnv(name)); -}); +/** Reads a single `SUPABASE_*` boolean env var from the ambient config provider (see module doc). */ +export const supabaseEnvBool = (name: string) => Effect.map(envValue(name), parseEnvBool); /** * Resolves a `SUPABASE_*` boolean where a project `supabase/.env` value may @@ -32,25 +28,23 @@ export const viperEnvBool = Effect.fnUntraced(function* (name: string) { * project env, letting an opt-out gate default on while any present value * still disables. */ -export const viperEnvBoolWithProjectFallback = Effect.fnUntraced(function* ( +export const supabaseEnvBoolWithProjectFallback = ( name: string, projectEnv: Record, opts: { readonly whenUnset?: boolean } = {}, -) { - const raw = (yield* shellEnv(name)) ?? projectEnv[name]; - if (raw === undefined) return opts.whenUnset ?? false; - return viperBool(raw); -}); +) => + Effect.map(envValue(name), (shell) => { + const raw = shell ?? projectEnv[name]; + return raw === undefined ? (opts.whenUnset ?? false) : parseEnvBool(raw); + }); /** * Resolves a `SUPABASE_*` string with the same shell-presence-wins semantics - * as {@link viperEnvBoolWithProjectFallback}, but with no boolean coercion — + * as {@link supabaseEnvBoolWithProjectFallback}, but with no boolean coercion — * the raw merged string, or `""` when absent from both. `??` (not `||`) * encodes the presence check. */ -export const viperEnvStringWithProjectFallback = Effect.fnUntraced(function* ( +export const supabaseEnvStringWithProjectFallback = ( name: string, projectEnv: Record, -) { - return (yield* shellEnv(name)) ?? projectEnv[name] ?? ""; -}); +) => Effect.map(envValue(name), (shell) => shell ?? projectEnv[name] ?? ""); diff --git a/apps/cli/src/command-internal/viper-env.unit.test.ts b/apps/cli/src/command-internal/supabase-env.unit.test.ts similarity index 53% rename from apps/cli/src/command-internal/viper-env.unit.test.ts rename to apps/cli/src/command-internal/supabase-env.unit.test.ts index e264ccb52d..e1dc9b34b6 100644 --- a/apps/cli/src/command-internal/viper-env.unit.test.ts +++ b/apps/cli/src/command-internal/supabase-env.unit.test.ts @@ -2,24 +2,24 @@ import { describe, expect, it } from "@effect/vitest"; import { ConfigProvider, Effect } from "effect"; import { - viperEnvBool, - viperEnvBoolWithProjectFallback, - viperEnvStringWithProjectFallback, -} from "./viper-env.ts"; + supabaseEnvBool, + supabaseEnvBoolWithProjectFallback, + supabaseEnvStringWithProjectFallback, +} from "./supabase-env.ts"; -const KEY = "SUPABASE_TEST_VIPER_BOOL"; -const STRING_KEY = "SUPABASE_TEST_VIPER_STRING"; +const KEY = "SUPABASE_TEST_ENV_BOOL"; +const STRING_KEY = "SUPABASE_TEST_ENV_STRING"; const withShellEnv = (env: Readonly>) => Effect.provide( ConfigProvider.layer(ConfigProvider.fromEnvRecord(env, { preserveEmptyStrings: true })), ); -describe("viperEnvBool", () => { - it.effect("is true only for strconv.ParseBool's true set (viper.GetBool parity)", () => +describe("supabaseEnvBool", () => { + it.effect("is true only for the boolean true set", () => Effect.gen(function* () { for (const value of ["1", "t", "T", "TRUE", "true", "True"]) { - expect(yield* viperEnvBool(KEY).pipe(withShellEnv({ [KEY]: value }))).toBe(true); + expect(yield* supabaseEnvBool(KEY).pipe(withShellEnv({ [KEY]: value }))).toBe(true); } }), ); @@ -27,35 +27,35 @@ describe("viperEnvBool", () => { it.effect("is false for the false set and any unrecognized value", () => Effect.gen(function* () { for (const value of ["0", "f", "F", "FALSE", "false", "False", "yes", "on", "", "nope"]) { - expect(yield* viperEnvBool(KEY).pipe(withShellEnv({ [KEY]: value }))).toBe(false); + expect(yield* supabaseEnvBool(KEY).pipe(withShellEnv({ [KEY]: value }))).toBe(false); } }), ); it.effect("is false when the env var is absent", () => Effect.gen(function* () { - expect(yield* viperEnvBool(KEY).pipe(withShellEnv({}))).toBe(false); + expect(yield* supabaseEnvBool(KEY).pipe(withShellEnv({}))).toBe(false); }), ); }); -describe("viperEnvBoolWithProjectFallback", () => { +describe("supabaseEnvBoolWithProjectFallback", () => { it.effect("falls back to the project value only when the shell var is absent", () => Effect.gen(function* () { expect( - yield* viperEnvBoolWithProjectFallback(KEY, { [KEY]: "true" }).pipe(withShellEnv({})), + yield* supabaseEnvBoolWithProjectFallback(KEY, { [KEY]: "true" }).pipe(withShellEnv({})), ).toBe(true); expect( - yield* viperEnvBoolWithProjectFallback(KEY, { [KEY]: "false" }).pipe(withShellEnv({})), + yield* supabaseEnvBoolWithProjectFallback(KEY, { [KEY]: "false" }).pipe(withShellEnv({})), ).toBe(false); - expect(yield* viperEnvBoolWithProjectFallback(KEY, {}).pipe(withShellEnv({}))).toBe(false); + expect(yield* supabaseEnvBoolWithProjectFallback(KEY, {}).pipe(withShellEnv({}))).toBe(false); }), ); it.effect("keeps a false shell override even when the project .env says true", () => Effect.gen(function* () { expect( - yield* viperEnvBoolWithProjectFallback(KEY, { [KEY]: "true" }).pipe( + yield* supabaseEnvBoolWithProjectFallback(KEY, { [KEY]: "true" }).pipe( withShellEnv({ [KEY]: "false" }), ), ).toBe(false); @@ -65,29 +65,27 @@ describe("viperEnvBoolWithProjectFallback", () => { it.effect("treats an empty shell value as present (blocks the project value) and false", () => Effect.gen(function* () { expect( - yield* viperEnvBoolWithProjectFallback(KEY, { [KEY]: "true" }).pipe( + yield* supabaseEnvBoolWithProjectFallback(KEY, { [KEY]: "true" }).pipe( withShellEnv({ [KEY]: "" }), ), ).toBe(false); }), ); - it.effect( - "treats an unparsable shell value as present and false (cast.ToBool swallows the error)", - () => - Effect.gen(function* () { - expect( - yield* viperEnvBoolWithProjectFallback(KEY, { [KEY]: "true" }).pipe( - withShellEnv({ [KEY]: "banana" }), - ), - ).toBe(false); - }), + it.effect("treats an unparsable shell value as present and false", () => + Effect.gen(function* () { + expect( + yield* supabaseEnvBoolWithProjectFallback(KEY, { [KEY]: "true" }).pipe( + withShellEnv({ [KEY]: "banana" }), + ), + ).toBe(false); + }), ); it.effect("keeps a true shell value over a false project value", () => Effect.gen(function* () { expect( - yield* viperEnvBoolWithProjectFallback(KEY, { [KEY]: "false" }).pipe( + yield* supabaseEnvBoolWithProjectFallback(KEY, { [KEY]: "false" }).pipe( withShellEnv({ [KEY]: "true" }), ), ).toBe(true); @@ -99,7 +97,7 @@ describe("viperEnvBoolWithProjectFallback", () => { () => Effect.gen(function* () { expect( - yield* viperEnvBoolWithProjectFallback(KEY, {}, { whenUnset: true }).pipe( + yield* supabaseEnvBoolWithProjectFallback(KEY, {}, { whenUnset: true }).pipe( withShellEnv({}), ), ).toBe(true); @@ -109,47 +107,49 @@ describe("viperEnvBoolWithProjectFallback", () => { it.effect("whenUnset: true still yields false for any present non-true value", () => Effect.gen(function* () { expect( - yield* viperEnvBoolWithProjectFallback(KEY, {}, { whenUnset: true }).pipe( + yield* supabaseEnvBoolWithProjectFallback(KEY, {}, { whenUnset: true }).pipe( withShellEnv({ [KEY]: "0" }), ), ).toBe(false); expect( - yield* viperEnvBoolWithProjectFallback(KEY, { [KEY]: "true" }, { whenUnset: true }).pipe( + yield* supabaseEnvBoolWithProjectFallback(KEY, { [KEY]: "true" }, { whenUnset: true }).pipe( withShellEnv({ [KEY]: "" }), ), ).toBe(false); expect( - yield* viperEnvBoolWithProjectFallback(KEY, {}, { whenUnset: true }).pipe( + yield* supabaseEnvBoolWithProjectFallback(KEY, {}, { whenUnset: true }).pipe( withShellEnv({ [KEY]: "banana" }), ), ).toBe(false); expect( - yield* viperEnvBoolWithProjectFallback(KEY, { [KEY]: "false" }, { whenUnset: true }).pipe( - withShellEnv({}), - ), + yield* supabaseEnvBoolWithProjectFallback( + KEY, + { [KEY]: "false" }, + { whenUnset: true }, + ).pipe(withShellEnv({})), ).toBe(false); }), ); }); -describe("viperEnvStringWithProjectFallback", () => { +describe("supabaseEnvStringWithProjectFallback", () => { it.effect("falls back to the project value only when the shell var is absent", () => Effect.gen(function* () { expect( - yield* viperEnvStringWithProjectFallback(STRING_KEY, { + yield* supabaseEnvStringWithProjectFallback(STRING_KEY, { [STRING_KEY]: "project-value", }).pipe(withShellEnv({})), ).toBe("project-value"); - expect(yield* viperEnvStringWithProjectFallback(STRING_KEY, {}).pipe(withShellEnv({}))).toBe( - "", - ); + expect( + yield* supabaseEnvStringWithProjectFallback(STRING_KEY, {}).pipe(withShellEnv({})), + ).toBe(""); }), ); it.effect("keeps the shell value over a project value", () => Effect.gen(function* () { expect( - yield* viperEnvStringWithProjectFallback(STRING_KEY, { + yield* supabaseEnvStringWithProjectFallback(STRING_KEY, { [STRING_KEY]: "project-value", }).pipe(withShellEnv({ [STRING_KEY]: "shell-value" })), ).toBe("shell-value"); @@ -159,20 +159,18 @@ describe("viperEnvStringWithProjectFallback", () => { it.effect("treats an empty shell value as present (blocks the project value)", () => Effect.gen(function* () { expect( - yield* viperEnvStringWithProjectFallback(STRING_KEY, { + yield* supabaseEnvStringWithProjectFallback(STRING_KEY, { [STRING_KEY]: "project-value", }).pipe(withShellEnv({ [STRING_KEY]: "" })), ).toBe(""); }), ); - it.effect( - "returns an empty string (not undefined) when absent from both, matching viper.GetString", - () => - Effect.gen(function* () { - expect( - yield* viperEnvStringWithProjectFallback(STRING_KEY, {}).pipe(withShellEnv({})), - ).toBe(""); - }), + it.effect("returns an empty string (not undefined) when absent from both", () => + Effect.gen(function* () { + expect( + yield* supabaseEnvStringWithProjectFallback(STRING_KEY, {}).pipe(withShellEnv({})), + ).toBe(""); + }), ); }); diff --git a/apps/cli/src/command-internal/target-default-warning.ts b/apps/cli/src/command-internal/target-default-warning.ts deleted file mode 100644 index 671c137dea..0000000000 --- a/apps/cli/src/command-internal/target-default-warning.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Effect, FileSystem, Option, Path } from "effect"; - -import { CommandSettings } from "../config/command-settings.service.ts"; -import { Output } from "../shared/output/output.service.ts"; -import { yellow } from "./colors.ts"; -import { readLinkSnapshotSoft } from "./link-snapshot.ts"; - -/** Warns on stderr that a run with no target selector but a configured linked project will default to the local target in the next major. */ -export const warnLinkedTargetDefault = Effect.fnUntraced(function* (input: { - readonly command: string; - readonly targetSelected: boolean; - readonly linkedRefResolved?: boolean; - readonly localTarget?: string; -}) { - if (input.targetSelected) return; - if (input.linkedRefResolved !== true) { - const cliSettings = yield* CommandSettings; - if (Option.isNone(Option.filter(cliSettings.projectId, (ref) => ref.length > 0))) { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const link = yield* readLinkSnapshotSoft(fs, path, cliSettings.workdir); - if (Option.isNone(link.ref)) return; - } - } - const output = yield* Output; - const localTarget = input.localTarget ?? "the local database"; - yield* output.raw( - `${yellow( - `Warning: supabase ${input.command} will target ${localTarget} by default in the next major version. Pass --linked to keep targeting the linked project.`, - )}\n`, - "stderr", - ); -}); diff --git a/apps/cli/src/command-internal/target-default-warning.unit.test.ts b/apps/cli/src/command-internal/target-default-warning.unit.test.ts deleted file mode 100644 index d1f27d95ed..0000000000 --- a/apps/cli/src/command-internal/target-default-warning.unit.test.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { BunServices } from "@effect/platform-bun"; -import { describe, expect, it } from "@effect/vitest"; -import { Effect, FileSystem, Layer, Option, Path } from "effect"; - -import { stripAnsi } from "../../tests/helpers/ansi.ts"; -import { VALID_REF, mockCommandSettings } from "../../tests/helpers/command-mocks.ts"; -import { mockOutput } from "../../tests/helpers/mocks.ts"; -import { targetDefaultWarning } from "../../tests/helpers/target-default-warning.ts"; -import { writeProjectLink } from "./project-link.ts"; -import { projectStateModeLayer } from "./project-state-mode.ts"; -import { warnLinkedTargetDefault } from "./target-default-warning.ts"; - -interface Scenario { - readonly projectId?: Option.Option; - readonly refFile?: string; - readonly refPathIsDirectory?: boolean; - readonly stackLinkRef?: string; -} - -const run = (input: Parameters[0], scenario: Scenario = {}) => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const workdir = yield* fs.makeTempDirectoryScoped({ prefix: "target-warning-" }); - const refPath = path.join(workdir, "supabase", ".temp", "project-ref"); - if (scenario.refFile !== undefined) { - yield* fs.makeDirectory(path.dirname(refPath), { recursive: true }); - yield* fs.writeFileString(refPath, scenario.refFile); - } - if (scenario.refPathIsDirectory === true) { - yield* fs.makeDirectory(refPath, { recursive: true }); - } - if (scenario.stackLinkRef !== undefined) { - yield* writeProjectLink(workdir, { - project: { ref: scenario.stackLinkRef }, - active_branch: { ref: scenario.stackLinkRef }, - fetchedAt: "2026-01-01T00:00:00.000Z", - }); - } - const out = mockOutput(); - yield* warnLinkedTargetDefault(input).pipe( - Effect.provide( - Layer.mergeAll( - out.layer, - mockCommandSettings({ workdir, projectId: scenario.projectId ?? Option.none() }), - BunServices.layer, - projectStateModeLayer(scenario.stackLinkRef === undefined ? "legacy" : "stack"), - ), - ), - ); - return { stdout: out.stdoutText, stderr: stripAnsi(out.stderrText) }; - }).pipe(Effect.provide(BunServices.layer)); - -const BARE = { command: "db push", targetSelected: false } as const; -const WARNING = `${targetDefaultWarning("db push")}\n`; - -describe("warnLinkedTargetDefault", () => { - it.effect("warns once on stderr when SUPABASE_PROJECT_ID configures the linked project", () => - Effect.gen(function* () { - const result = yield* run(BARE, { projectId: Option.some(VALID_REF) }); - expect(result).toEqual({ stdout: "", stderr: WARNING }); - }), - ); - - it.effect("warns when only the link file configures the linked project", () => - Effect.gen(function* () { - const result = yield* run(BARE, { refFile: `${VALID_REF}\n` }); - expect(result).toEqual({ stdout: "", stderr: WARNING }); - }), - ); - - it.effect("warns when a stack-mode link record configures the linked project", () => - Effect.gen(function* () { - const result = yield* run(BARE, { stackLinkRef: VALID_REF }); - expect(result).toEqual({ stdout: "", stderr: WARNING }); - }), - ); - - it.effect("stays silent when neither SUPABASE_PROJECT_ID nor the link file is configured", () => - Effect.gen(function* () { - const result = yield* run(BARE); - expect(result).toEqual({ stdout: "", stderr: "" }); - }), - ); - - it.effect("stays silent when the link file is blank", () => - Effect.gen(function* () { - const result = yield* run(BARE, { refFile: " \n" }); - expect(result.stderr).toBe(""); - }), - ); - - it.effect("stays silent when the link file cannot be read", () => - Effect.gen(function* () { - const result = yield* run(BARE, { refPathIsDirectory: true }); - expect(result.stderr).toBe(""); - }), - ); - - it.effect("stays silent when a target selector was passed, even with a linked project", () => - Effect.gen(function* () { - const result = yield* run( - { command: "db push", targetSelected: true }, - { projectId: Option.some(VALID_REF), refFile: VALID_REF }, - ); - expect(result.stderr).toBe(""); - }), - ); - - it.effect("warns when the caller already resolved the ref and nothing is configured", () => - Effect.gen(function* () { - const result = yield* run({ ...BARE, linkedRefResolved: true }); - expect(result).toEqual({ stdout: "", stderr: WARNING }); - }), - ); - - it.effect("stays silent for a resolved ref when a target selector was passed", () => - Effect.gen(function* () { - const result = yield* run({ - command: "db push", - targetSelected: true, - linkedRefResolved: true, - }); - expect(result.stderr).toBe(""); - }), - ); - - it.effect("names the local target the caller supplies", () => - Effect.gen(function* () { - const result = yield* run( - { command: "storage ls", targetSelected: false, localTarget: "the local Storage API" }, - { projectId: Option.some(VALID_REF) }, - ); - expect(result.stderr).toBe( - `${targetDefaultWarning("storage ls", "the local Storage API")}\n`, - ); - }), - ); -}); diff --git a/apps/cli/src/command-internal/target/branch-resolution.errors.ts b/apps/cli/src/command-internal/target/branch-resolution.errors.ts new file mode 100644 index 0000000000..893b775840 --- /dev/null +++ b/apps/cli/src/command-internal/target/branch-resolution.errors.ts @@ -0,0 +1,71 @@ +import { Data } from "effect"; +import { + actionability, + type CliErrorActionabilityDeclaration, + ErrorActionabilityId, + statusCodeActionability, +} from "../../shared/telemetry/error-actionability.ts"; + +/** A branch name was given but no linked parent project could be resolved to search for it. */ +export class LinkBranchNotLinkedError extends Data.TaggedError("LinkBranchNotLinkedError")<{ + readonly message: string; + readonly suggestion?: string; +}> { + get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { + return actionability.projectNotLinked; + } +} + +/** A linked-parent candidate (env, cache or `project-ref` file) exists but none is ref-shaped. */ +export class LinkParentRefInvalidError extends Data.TaggedError("LinkParentRefInvalidError")<{ + readonly message: string; + readonly suggestion?: string; +}> { + get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { + return actionability.relinkProject; + } +} + +/** No branch with the given name/UUID exists on the resolved parent project. */ +export class LinkBranchNotFoundError extends Data.TaggedError("LinkBranchNotFoundError")<{ + readonly message: string; + readonly suggestion?: string; +}> { + get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { + return actionability.invalidInput; + } +} + +/** The branch has no `project_ref` yet (e.g. `CREATING_PROJECT`), so the lookup stops there. */ +export class LinkBranchNotReadyError extends Data.TaggedError("LinkBranchNotReadyError")<{ + readonly branch: string; + readonly status: string; + readonly message: string; +}> { + get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { + return { ...actionability.apiStatus, fingerprint_suffix: "branch_not_ready" }; + } +} + +/** Transport or response-decode failure while listing branches or fetching one by UUID. */ +export class LinkBranchListNetworkError extends Data.TaggedError("LinkBranchListNetworkError")<{ + readonly message: string; + readonly decode?: boolean; +}> { + get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { + return this.decode === true + ? { ...actionability.apiStatus, fingerprint_suffix: "api_response" } + : actionability.externalNetwork; + } +} + +/** `GET /v1/projects/{ref}/branches` or `GET /v1/branches/{id}` returned a non-200 status. */ +export class LinkBranchListStatusError extends Data.TaggedError("LinkBranchListStatusError")<{ + readonly status: number; + readonly body: string; + readonly message: string; +}> { + get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { + return statusCodeActionability(this.status, { notFoundIsInvalidInput: true }); + } +} diff --git a/apps/cli/src/command-internal/target/branch-resolution.errors.unit.test.ts b/apps/cli/src/command-internal/target/branch-resolution.errors.unit.test.ts new file mode 100644 index 0000000000..672e86eabf --- /dev/null +++ b/apps/cli/src/command-internal/target/branch-resolution.errors.unit.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from "vitest"; + +import { classifyCliErrorActionability } from "../../shared/telemetry/error-actionability.ts"; +import { + LinkBranchListNetworkError, + LinkBranchListStatusError, + LinkBranchNotReadyError, + LinkParentRefInvalidError, +} from "./branch-resolution.errors.ts"; + +describe("branch resolution actionability", () => { + it("classifies a branch-list 404 as invalid input, same policy as the api-keys 404", () => { + const result = classifyCliErrorActionability( + new LinkBranchListStatusError({ status: 404, body: "ignored", message: "ignored" }), + ); + expect(result.error_kind).toBe("user_actionable"); + expect(result.error_category).toBe("invalid_input"); + expect(result.error_fingerprint).toBe("tag:LinkBranchListStatusError:not_found"); + }); + + it("keeps a non-404 branch-list status on the API-status policy", () => { + const result = classifyCliErrorActionability( + new LinkBranchListStatusError({ status: 500, body: "ignored", message: "ignored" }), + ); + expect(result.error_kind).toBe("external_service"); + expect(result.error_category).toBe("api_status"); + expect(result.error_fingerprint).toBe("tag:LinkBranchListStatusError:api_status"); + }); + + it("classifies a branch-list body-decode failure as an API response problem", () => { + const result = classifyCliErrorActionability( + new LinkBranchListNetworkError({ message: "boom", decode: true }), + ); + expect(result.error_kind).toBe("external_service"); + expect(result.error_category).toBe("api_status"); + expect(result.error_fingerprint).toBe("tag:LinkBranchListNetworkError:api_response"); + }); + + it("classifies a branch-list transport failure as network", () => { + const result = classifyCliErrorActionability( + new LinkBranchListNetworkError({ message: "boom" }), + ); + expect(result.error_kind).toBe("external_service"); + expect(result.error_category).toBe("network"); + }); + + it("classifies an invalid linked parent ref as a relink-project remediation", () => { + const result = classifyCliErrorActionability( + new LinkParentRefInvalidError({ message: "ignored" }), + ); + expect(result.error_kind).toBe("user_actionable"); + expect(result.error_category).toBe("invalid_config"); + expect(result.suggestion_type).toBe("link_project"); + expect(result.suggested_command).toBe("supabase link"); + }); + + it("classifies a not-yet-provisioned branch as an API-status problem with its own fingerprint", () => { + const result = classifyCliErrorActionability( + new LinkBranchNotReadyError({ + branch: "feature-branch", + status: "CREATING_PROJECT", + message: "ignored", + }), + ); + expect(result.error_kind).toBe("external_service"); + expect(result.error_category).toBe("api_status"); + expect(result.error_fingerprint).toBe("tag:LinkBranchNotReadyError:branch_not_ready"); + }); +}); diff --git a/apps/cli/src/command-internal/target/branch-resolution.ts b/apps/cli/src/command-internal/target/branch-resolution.ts new file mode 100644 index 0000000000..e81c23178f --- /dev/null +++ b/apps/cli/src/command-internal/target/branch-resolution.ts @@ -0,0 +1,300 @@ +import type { V1ListAllBranchesOutput } from "@supabase/api/effect"; +import { Effect, Option } from "effect"; + +import { CommandPlatformApiFactory } from "../../auth/command-platform-api-factory.service.ts"; +import { CommandPlatformApi } from "../../auth/command-platform-api.service.ts"; +import { PROJECT_REF_PATTERN } from "../../config/project-ref.service.ts"; +import { Output } from "../../shared/output/output.service.ts"; +import { block } from "../command-permissions/model.ts"; +import { + AUTHENTICATION_FAILED_STATUS_MESSAGE, + mapHttpError, + sanitizeInlineName, +} from "../http-errors.ts"; +import { + parentNotLinkedMessage, + parentRefInvalidMessage, + parentRefTypoHint, + resolveLinkedParentRef, + type ParentRefResolution, +} from "../parent-project-ref.ts"; +import { BRANCH_UUID_PATTERN } from "../ref-patterns.ts"; +import { + LinkBranchListNetworkError, + LinkBranchListStatusError, + LinkBranchNotFoundError, + LinkBranchNotLinkedError, + LinkBranchNotReadyError, + LinkParentRefInvalidError, +} from "./branch-resolution.errors.ts"; +import { closestByName } from "./closest-branch.ts"; +import { stripUserinfo } from "./remote-vocabulary.ts"; + +type Branches = typeof V1ListAllBranchesOutput.Type; +export type ParentBranch = Branches[number]; + +const MAX_LISTED_BRANCHES = 20; + +/** The branch lookups every `--remote` value that names a branch makes. */ +export const branchResolution = block("branch-resolution", [ + { + operationId: "v1-list-all-branches", + kind: "required", + when: [{ selector: ["branch", "uuid"] }], + context: "a project is linked, or the value is a branch name", + }, + { + operationId: "v1-get-a-branch-config", + kind: "required", + when: [{ selector: ["uuid"] }], + context: "no linked project resolves, so the branch is looked up by its UUID", + }, + { + operationId: "v1-list-all-branches", + kind: "required", + when: [{ selector: ["bare"] }], + context: + "a bare --remote is followed by an operand that could name a branch of the linked project", + }, +]); + +/** A branch resolved to its own project ref, with the parent project it was found under. */ +export interface BranchResolution { + readonly projectRef: string; + readonly branchName: string; + readonly parentRef: string; + /** The project's production branch, which is the linked project itself. */ + readonly isDefault: boolean; +} + +function branchNotFoundMessage(value: string, parentRef: string, branches: Branches): string { + if (branches.length === 0) { + return `Branch "${value}" not found: project ${parentRef} has no branches.${parentRefTypoHint(value)}`; + } + + const sortedNames = branches.map((branch) => branch.name).toSorted(); + const shown = sortedNames.slice(0, MAX_LISTED_BRANCHES); + const remaining = sortedNames.length - shown.length; + // Branch names are API-provided; sanitize them the same way response bodies are before + // embedding them in an error message. + const shownSanitized = sanitizeInlineName(shown.join(", ")); + const namesList = + remaining > 0 + ? `${shownSanitized}, … (${remaining} more — run supabase branches list)` + : shownSanitized; + + const closest = closestByName(branches, value); + // Sanitized like the list above — an API-provided name must not be able to inject + // ANSI/OSC/newline controls into the terminal. + const didYouMean = + closest !== undefined ? ` Did you mean "${sanitizeInlineName(closest.name)}"?` : ""; + + return ( + `Branch "${value}" not found for project ${parentRef}. Available branches: ${namesList}.` + + `${didYouMean}${parentRefTypoHint(value)}` + ); +} + +/** The production branch is the linked project itself. */ +export const isDefaultBranch = (branch: ParentBranch, parentRef: string): boolean => + branch.is_default || branch.project_ref === parentRef; + +/** UUIDs match case-insensitively, names case-sensitively; the not-found hint covers typos. */ +export const findBranch = (branches: Branches, value: string): ParentBranch | undefined => + branches.find( + (branch) => branch.name === value || branch.id.toLowerCase() === value.toLowerCase(), + ); + +const unresolvedParent = Effect.fnUntraced(function* ( + parent: Exclude, + value: string, +) { + return parent.kind === "absent" + ? yield* new LinkBranchNotLinkedError({ + message: parentNotLinkedMessage(stripUserinfo(value)), + }) + : yield* new LinkParentRefInvalidError({ + message: parentRefInvalidMessage(stripUserinfo(value)), + }); +}); + +/** The command's eager client, else the lazy factory's, so a bare run never resolves a token. */ +const acquireApi = Effect.fnUntraced(function* () { + const direct = yield* Effect.serviceOption(CommandPlatformApi); + if (Option.isSome(direct)) return direct.value; + const factory = yield* Effect.serviceOption(CommandPlatformApiFactory); + if (Option.isNone(factory)) { + return yield* Effect.die( + new Error("a branch lookup needs CommandPlatformApi or CommandPlatformApiFactory"), + ); + } + return yield* factory.value.make; +}); + +/** + * A branch-lookup spinner, shown only on a terminal: a non-terminal text run writes task steps to + * stdout, which may carry the command's payload. + */ +const startBranchLookupTask = Effect.fnUntraced(function* (label: string) { + const output = yield* Output; + return output.format === "text" && output.interactive ? yield* output.task(label) : undefined; +}); + +const LIST_BRANCHES_FAILED = "failed to list branches: "; + +/** A branch-list error's transport failure, without its `failed to list branches: ` lead. */ +export const listBranchesFailureCause = (error: LinkBranchListNetworkError): string => { + const [firstLine = ""] = error.message.split("\n"); + return firstLine.startsWith(LIST_BRANCHES_FAILED) + ? firstLine.slice(LIST_BRANCHES_FAILED.length) + : firstLine; +}; + +/** What to try when the listed project is itself a preview branch, which has no branches. */ +export const previewBranchHint = (ref: string): string => + `If ${ref} is itself a preview branch, link its parent project first: supabase link --project-ref `; + +/** `GET /v1/projects/{ref}/branches` for every branch-name lookup; no `label` means no spinner. */ +const listParentBranches = Effect.fnUntraced(function* (parentRef: string, label?: string) { + const api = yield* acquireApi(); + + const mapBranchListError = mapHttpError({ + networkError: LinkBranchListNetworkError, + statusError: LinkBranchListStatusError, + networkMessage: (cause) => `${LIST_BRANCHES_FAILED}${cause}`, + statusMessage: (status, body) => { + if (status === 401) return AUTHENTICATION_FAILED_STATUS_MESSAGE; + return status === 404 + ? `Cannot list branches for project ${parentRef} (HTTP 404). ${previewBranchHint(parentRef)}` + : `unexpected list branches status ${status}: ${body}`; + }, + }); + + const task = label === undefined ? undefined : yield* startBranchLookupTask(label); + const branches: Branches = yield* api.v1.listAllBranches({ ref: parentRef }).pipe( + Effect.tapError(() => task?.fail() ?? Effect.void), + Effect.catch(mapBranchListError), + ); + yield* task?.clear ?? Effect.void; + return branches; +}); + +export type ResolvedParent = Extract; + +/** + * The branches of the parent a lookup runs under. A cached parent stands only while it lists the + * linked project as a branch; otherwise the linked project's own branches are used, unless they + * cannot be listed, as for a deleted branch's project, which keeps the cached parent. + */ +export const listLinkedParentBranches = Effect.fnUntraced(function* ( + parent: ResolvedParent, + label?: string, +) { + const cached = yield* listParentBranches(parent.ref, label); + const { linkedRef } = parent; + if (linkedRef === undefined || cached.some((branch) => branch.project_ref === linkedRef)) { + return { parentRef: parent.ref, branches: cached }; + } + const own = yield* listParentBranches(linkedRef).pipe(Effect.option); + return Option.isSome(own) + ? { parentRef: linkedRef, branches: own.value } + : { parentRef: parent.ref, branches: cached }; +}); + +const resolveInParent = Effect.fnUntraced(function* (value: string, parent: ResolvedParent) { + const { parentRef, branches } = yield* listLinkedParentBranches(parent, "Resolving branch..."); + yield* Effect.annotateCurrentSpan({ + "project.parent_ref": parentRef, + "branch.count": branches.length, + }); + + const found = findBranch(branches, value); + if (found === undefined) { + return yield* new LinkBranchNotFoundError({ + message: branchNotFoundMessage(stripUserinfo(value), parentRef, branches), + }); + } + + if (!PROJECT_REF_PATTERN.test(found.project_ref)) { + return yield* new LinkBranchNotReadyError({ + branch: found.name, + status: found.status, + message: `Branch "${sanitizeInlineName(found.name)}" has no project ref yet (status: ${found.status}). Wait for it to finish provisioning, then retry.`, + }); + } + + return { + projectRef: found.project_ref, + branchName: found.name, + parentRef, + isDefault: isDefaultBranch(found, parentRef), + } satisfies BranchResolution; +}); + +/** + * Resolves a non-ref-shaped value to the branch's project ref by looking it up (by name or UUID) + * against the linked parent project's branches. A ref-shaped value (20 lowercase letters) is + * always a ref and never looked up as a branch name. + * + * Uses the list endpoint rather than a single-branch lookup so the full list can power the + * available-branches error. + */ +export const resolveLinkBranchRef = Effect.fn("Target.resolveBranch")(function* (value: string) { + const parent = yield* resolveLinkedParentRef(); + if (parent.kind !== "resolved") return yield* unresolvedParent(parent, value); + return yield* resolveInParent(value, parent); +}); + +const resolveBranchById = Effect.fnUntraced(function* (id: string) { + const api = yield* acquireApi(); + const notFoundMessage = `Branch "${sanitizeInlineName(id)}" not found. Run \`supabase branches list\` to see available branches.`; + const mapBranchGetError = mapHttpError({ + networkError: LinkBranchListNetworkError, + statusError: LinkBranchListStatusError, + networkMessage: (cause) => `failed to look up branch: ${cause}`, + statusMessage: (status, body) => { + if (status === 401) return AUTHENTICATION_FAILED_STATUS_MESSAGE; + return status === 404 ? notFoundMessage : `unexpected get branch status ${status}: ${body}`; + }, + }); + + const task = yield* startBranchLookupTask("Resolving branch..."); + const detail = yield* api.v1.getABranchConfig({ branch_id_or_ref: id }).pipe( + Effect.tapError(() => task?.fail() ?? Effect.void), + Effect.catch(mapBranchGetError), + Effect.catchTag("SupabaseApiInputError", () => + Effect.fail( + new LinkBranchListStatusError({ status: 404, body: "", message: notFoundMessage }), + ), + ), + ); + yield* task?.clear ?? Effect.void; + + if (!PROJECT_REF_PATTERN.test(detail.ref)) { + return yield* new LinkBranchNotReadyError({ + branch: id, + status: detail.status, + message: `Branch "${sanitizeInlineName(id)}" has no project ref yet (status: ${detail.status}). Wait for it to finish provisioning, then retry.`, + }); + } + return { + projectRef: detail.ref, + branchName: undefined, + parentRef: undefined, + // `GET /v1/branches/{id}` does not say whether this is the production branch. + isDefault: undefined, + }; +}); + +/** + * Resolves the branch a `--remote` value names: against the linked project's branches when a + * project is linked, and a UUID with no usable link through `GET /v1/branches/{id}`. + */ +export const resolveRemoteBranchRef = Effect.fn("Target.resolveRemoteBranch")(function* ( + value: string, +) { + const parent = yield* resolveLinkedParentRef(); + if (parent.kind === "resolved") return yield* resolveInParent(value, parent); + if (BRANCH_UUID_PATTERN.test(value)) return yield* resolveBranchById(value); + return yield* unresolvedParent(parent, value); +}); diff --git a/apps/cli/src/command-internal/target/closest-branch.ts b/apps/cli/src/command-internal/target/closest-branch.ts new file mode 100644 index 0000000000..80c595c140 --- /dev/null +++ b/apps/cli/src/command-internal/target/closest-branch.ts @@ -0,0 +1,91 @@ +/** Optimal string alignment distance: an insertion, deletion, substitution or swap is one edit. */ +const editDistance = (a: string, b: string): number => { + let beforePrevious: ReadonlyArray = []; + let previous = Array.from({ length: b.length + 1 }, (_, index) => index); + for (let row = 1; row <= a.length; row += 1) { + const current = [row]; + for (let column = 1; column <= b.length; column += 1) { + let distance = Math.min( + (previous[column] ?? 0) + 1, + (current[column - 1] ?? 0) + 1, + (previous[column - 1] ?? 0) + (a[row - 1] === b[column - 1] ? 0 : 1), + ); + if (row > 1 && column > 1 && a[row - 1] === b[column - 2] && a[row - 2] === b[column - 1]) { + distance = Math.min(distance, (beforePrevious[column - 2] ?? 0) + 1); + } + current[column] = distance; + } + beforePrevious = previous; + previous = current; + } + return previous[b.length] ?? 0; +}; + +/** Up to two edits, fewer for a short name, so two unrelated short words never match. */ +const allowedEdits = (a: string, b: string): number => + Math.min(2, Math.floor(Math.max(a.length, b.length) / 3)); + +const byName = (a: { readonly name: string }, b: { readonly name: string }): number => + a.name < b.name ? -1 : a.name > b.name ? 1 : 0; + +/** The item named `word` ignoring case or within two edits, nearest first, then by name. */ +export const closestByName = ( + items: ReadonlyArray, + word: string, +): T | undefined => { + const target = word.trim().toLowerCase(); + let best: { readonly item: T; readonly distance: number } | undefined; + for (const item of items.toSorted(byName)) { + const name = item.name.toLowerCase(); + const distance = editDistance(target, name); + if ( + distance <= allowedEdits(target, name) && + (best === undefined || distance < best.distance) + ) { + best = { item, distance }; + } + } + return best?.item; +}; + +const MIN_NEAR_MISS_NAME_LENGTH = 5; + +/** + * The item a word most likely mistypes: one whose name matches ignoring case, else a non-default + * one of at least five characters a single edit away. A short or default name never claims a word + * that reads as an ordinary operand. + */ +export const nearMissByName = ( + items: ReadonlyArray, + word: string, + isDefault: (item: T) => boolean, +): T | undefined => { + const target = word.toLowerCase(); + const sorted = items.toSorted(byName); + return ( + sorted.find((item) => item.name.toLowerCase() === target) ?? + sorted.find( + (item) => + !isDefault(item) && + item.name.length >= MIN_NEAR_MISS_NAME_LENGTH && + editDistance(target, item.name.toLowerCase()) <= 1, + ) + ); +}; + +/** The word within `maxEdits` of `word`, ignoring case; the nearest wins, then the earlier one. */ +export const closestWord = ( + words: ReadonlyArray, + word: string, + maxEdits: number, +): string | undefined => { + const target = word.trim().toLowerCase(); + let best: { readonly word: string; readonly distance: number } | undefined; + for (const candidate of words) { + const distance = editDistance(target, candidate.toLowerCase()); + if (distance <= maxEdits && (best === undefined || distance < best.distance)) { + best = { word: candidate, distance }; + } + } + return best?.word; +}; diff --git a/apps/cli/src/command-internal/target/closest-branch.unit.test.ts b/apps/cli/src/command-internal/target/closest-branch.unit.test.ts new file mode 100644 index 0000000000..89c3c7602b --- /dev/null +++ b/apps/cli/src/command-internal/target/closest-branch.unit.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, it } from "vitest"; + +import { closestByName, closestWord, nearMissByName } from "./closest-branch.ts"; + +const branches = ["feature-x", "staging", "Main", "add_profiles", "db"].map((name) => ({ name })); + +describe("closestByName", () => { + it.each([ + { word: "feature-y", expected: "feature-x" }, + { word: "Feature-X", expected: "feature-x" }, + { word: "stagin", expected: "staging" }, + { word: "stagign", expected: "staging" }, + { word: "mian", expected: "Main" }, + { word: "staging2", expected: "staging" }, + { word: "main", expected: "Main" }, + { word: "add_profile", expected: "add_profiles" }, + { word: " staging ", expected: "staging" }, + ])("finds $expected for $word", ({ word, expected }) => { + expect(closestByName(branches, word)?.name).toBe(expected); + }); + + it.each(["production", "feature-xyz-long", "x", "dx", "fi"])( + "finds nothing for %s, which is more than two edits from every name", + (word) => { + expect(closestByName(branches, word)).toBeUndefined(); + }, + ); + + it("lets a short word match only by case", () => { + expect(closestByName(branches, "DB")?.name).toBe("db"); + expect(closestByName(branches, "dc")).toBeUndefined(); + }); + + it("prefers the fewest edits, then the first name alphabetically", () => { + const names = [{ name: "release-b" }, { name: "release-a" }, { name: "release-ab" }]; + expect(closestByName(names, "release-c")?.name).toBe("release-a"); + expect(closestByName(names, "release-a")?.name).toBe("release-a"); + }); + + it("finds nothing among no branches", () => { + expect(closestByName([], "staging")).toBeUndefined(); + }); +}); + +describe("nearMissByName", () => { + const projectBranches = [ + { name: "main", isDefault: true }, + ...["develop", "staging", "app", "dev", "prod"].map((name) => ({ name, isDefault: false })), + ]; + const nearMiss = (word: string) => + nearMissByName(projectBranches, word, (branch) => branch.isDefault)?.name; + + it.each(["mail", "man", "rain", "api", "pro", "devel"])( + "leaves %s an ordinary word, being close only to short names, the default branch or neither", + (word) => { + expect(nearMiss(word)).toBeUndefined(); + }, + ); + + it.each([ + { word: "stagin", expected: "staging" }, + { word: "stagign", expected: "staging" }, + { word: "Dev", expected: "dev" }, + { word: "dev", expected: "dev" }, + { word: "MAIN", expected: "main" }, + ])("takes $word for $expected", ({ word, expected }) => { + expect(nearMiss(word)).toBe(expected); + }); + + it("takes the first name alphabetically when several match ignoring case", () => { + const names = [{ name: "release-b" }, { name: "Release-A" }, { name: "release-a" }]; + expect(nearMissByName(names, "RELEASE-A", () => false)?.name).toBe("Release-A"); + }); + + it("finds nothing among no branches", () => { + expect(nearMissByName([], "staging", () => false)).toBeUndefined(); + }); +}); + +describe("closestWord", () => { + const words = ["local", "migrations", "remote"]; + + it.each([ + { word: "lcoal", expected: "local" }, + { word: "LOCAL", expected: "local" }, + { word: "loc", expected: "local" }, + { word: "locall", expected: "local" }, + { word: "remove", expected: "remote" }, + { word: "migraitons", expected: "migrations" }, + { word: " remtoe ", expected: "remote" }, + ])("finds $expected for $word within two edits", ({ word, expected }) => { + expect(closestWord(words, word, 2)).toBe(expected); + }); + + it.each(["lo", "main", "feature-x", "migrate", ""])( + "finds nothing for %s, which is more than two edits from every word", + (word) => { + expect(closestWord(words, word, 2)).toBeUndefined(); + }, + ); + + it("takes the nearest word, then the earlier one", () => { + expect(closestWord(["abcd", "abce"], "abcf", 2)).toBe("abcd"); + expect(closestWord(["abxx", "abcx"], "abcd", 2)).toBe("abcx"); + }); +}); diff --git a/apps/cli/src/command-internal/target/parent-scoped-project-ref.integration.test.ts b/apps/cli/src/command-internal/target/parent-scoped-project-ref.integration.test.ts new file mode 100644 index 0000000000..8d6b8a6679 --- /dev/null +++ b/apps/cli/src/command-internal/target/parent-scoped-project-ref.integration.test.ts @@ -0,0 +1,192 @@ +import { describe, expect, it } from "@effect/vitest"; +import { Effect, FileSystem, Option, Path } from "effect"; + +import { + buildTestRuntime, + jsonResponse, + mockCommandPlatformApi, + mockCommandSettings, + unverifiedTargetBanner, + useTempWorkdir, +} from "../../../tests/helpers/command-mocks.ts"; +import { mockOutput } from "../../../tests/helpers/mocks.ts"; +import { assertBlockDrift } from "../../../tests/helpers/permission-drift.ts"; +import { branchRow } from "../../../tests/helpers/remote-selection.ts"; +import { branchesParentTarget } from "../../commands/branches/branches.target.ts"; +import type { RemoteValue } from "./remote-value.ts"; +import { resolveProjectTarget } from "./resolve-project-target.ts"; + +const CACHED_PARENT = "cachedparentprojecta"; +const LINKED_REF = "linkedprojectrefabcd"; +const OTHER_BRANCH_REF = "otherbranchrefabcdef"; +const ENV_REF = "envprojectrefaaaaaaa"; +const FLAG_REF = "flagprojectrefabcdef"; +const API = "https://api.supabase.com"; + +const tempRoot = useTempWorkdir("supabase-parent-scoped-ref-int-"); + +interface Listing { + readonly status: number; + readonly body: unknown; +} + +const parentBranch = (projectRef: string) => + branchRow(`branch-${projectRef}`, { project_ref: projectRef, parent_project_ref: CACHED_PARENT }); + +const branches = (...refs: ReadonlyArray): Listing => ({ + status: 200, + body: refs.map(parentBranch), +}); + +const failure = (status: number): Listing => ({ status, body: { message: "failed" } }); + +const listingRequest = (projectRef: string) => `GET ${API}/v1/projects/${projectRef}/branches`; + +interface LinkState { + readonly file?: string; + readonly cache?: string; + readonly env?: string; + readonly remote?: RemoteValue; +} + +interface DifferingCacheCase { + readonly name: string; + readonly listings: Readonly>; + readonly expected: { readonly ref: string; readonly requests: string[]; readonly banner: string }; +} + +interface NoCheckCase { + readonly name: string; + readonly state: LinkState; + readonly ref: string; +} + +function resolveParent(state: LinkState, listings: Readonly>) { + const out = mockOutput(); + const api = mockCommandPlatformApi({ + handler: (request) => { + const projectRef = /\/v1\/projects\/([a-z]{20})\/branches$/.exec(request.url)?.[1]; + const listing = projectRef === undefined ? undefined : listings[projectRef]; + return Effect.succeed( + listing === undefined + ? jsonResponse(request, 404, { message: "unexpected request" }) + : jsonResponse(request, listing.status, listing.body), + ); + }, + }); + const layer = buildTestRuntime({ + out, + api, + cliSettings: mockCommandSettings({ + workdir: tempRoot.current, + projectId: Option.fromNullishOr(state.env), + }), + }); + return Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const tempDir = path.join(tempRoot.current, "supabase", ".temp"); + yield* fs.makeDirectory(tempDir, { recursive: true }); + if (state.file !== undefined) { + yield* fs.writeFileString(path.join(tempDir, "project-ref"), state.file); + } + if (state.cache !== undefined) { + yield* fs.writeFileString( + path.join(tempDir, "linked-project.json"), + `{"ref":"${state.cache}","name":"Parent Project","organization_id":"org_1","organization_slug":"acme"}`, + ); + } + const ref = yield* resolveProjectTarget( + { remote: Option.fromNullishOr(state.remote), projectRef: Option.none() }, + { parent: true }, + ); + assertBlockDrift(branchesParentTarget, { requests: api.requests }); + return { + ref, + requests: api.requests.map(({ method, url }) => `${method} ${url}`), + banner: out.stderrText, + }; + }).pipe(Effect.provide(layer)); +} + +describe("a parent-scoped command whose cached parent differs from the linked project", () => { + const cachedParentBanner = `Target: project Parent Project (${CACHED_PARENT}) · production\n`; + + const differingCacheCases: ReadonlyArray = [ + { + name: "keeps the cached parent while it lists the linked project as a branch", + listings: { [CACHED_PARENT]: branches(OTHER_BRANCH_REF, LINKED_REF) }, + expected: { + ref: CACHED_PARENT, + requests: [listingRequest(CACHED_PARENT)], + banner: cachedParentBanner, + }, + }, + { + name: "switches to the linked project, announced without the cache's name, once the cached parent no longer lists it", + listings: { [CACHED_PARENT]: branches(OTHER_BRANCH_REF), [LINKED_REF]: branches() }, + expected: { + ref: LINKED_REF, + requests: [listingRequest(CACHED_PARENT), listingRequest(LINKED_REF)], + banner: unverifiedTargetBanner(LINKED_REF), + }, + }, + { + name: "keeps the cached parent when the linked project's own branches cannot be listed", + listings: { [CACHED_PARENT]: branches(OTHER_BRANCH_REF), [LINKED_REF]: failure(403) }, + expected: { + ref: CACHED_PARENT, + requests: [listingRequest(CACHED_PARENT), listingRequest(LINKED_REF)], + banner: cachedParentBanner, + }, + }, + { + name: "keeps the cached parent when its own branches cannot be listed", + listings: { [CACHED_PARENT]: failure(500), [LINKED_REF]: branches() }, + expected: { + ref: CACHED_PARENT, + requests: [listingRequest(CACHED_PARENT)], + banner: cachedParentBanner, + }, + }, + ]; + + it.effect.each(differingCacheCases)("$name", ({ listings, expected }) => + Effect.gen(function* () { + const resolved = yield* resolveParent({ file: LINKED_REF, cache: CACHED_PARENT }, listings); + expect(resolved).toEqual(expected); + }), + ); + + const noCheckCases: ReadonlyArray = [ + { + name: "there is no cache", + state: { file: LINKED_REF }, + ref: LINKED_REF, + }, + { + name: "the cache names the linked project itself", + state: { file: CACHED_PARENT, cache: CACHED_PARENT }, + ref: CACHED_PARENT, + }, + { + name: "SUPABASE_PROJECT_ID names the project", + state: { file: LINKED_REF, cache: CACHED_PARENT, env: ENV_REF }, + ref: ENV_REF, + }, + { + name: "--remote= names the project", + state: { file: LINKED_REF, cache: CACHED_PARENT, remote: { _tag: "Ref", ref: FLAG_REF } }, + ref: FLAG_REF, + }, + ]; + + it.effect.each(noCheckCases)( + "lists no branches to check the cache when $name", + ({ state, ref }) => + Effect.gen(function* () { + const resolved = yield* resolveParent(state, {}); + expect(resolved).toMatchObject({ ref, requests: [] }); + }), + ); +}); diff --git a/apps/cli/src/command-internal/target/parent-scoped-project-ref.ts b/apps/cli/src/command-internal/target/parent-scoped-project-ref.ts new file mode 100644 index 0000000000..27930fef6e --- /dev/null +++ b/apps/cli/src/command-internal/target/parent-scoped-project-ref.ts @@ -0,0 +1,51 @@ +import { Effect, FileSystem, Option, Path } from "effect"; + +import { CommandSettings } from "../../config/command-settings.service.ts"; +import { ProjectRefResolver } from "../../config/project-ref.service.ts"; +import { announceTarget } from "../../shared/output/command-target.service.ts"; +import { readCachedLinkedProject, resolveLinkedParentRef } from "../parent-project-ref.ts"; +import { listLinkedParentBranches, type ResolvedParent } from "./branch-resolution.ts"; +import { describeLinkedTarget } from "./target-identity.ts"; + +/** The cache outlives a re-link, so it stands only while it lists the linked project. */ +const checkedParentRef = Effect.fnUntraced(function* (parent: ResolvedParent) { + if (parent.linkedRef === undefined) return parent.ref; + const { parentRef } = yield* listLinkedParentBranches(parent).pipe( + Effect.orElseSucceed(() => ({ parentRef: parent.ref })), + ); + yield* Effect.annotateCurrentSpan("parent_project_ref.cache_stale", parentRef !== parent.ref); + return parentRef; +}); + +/** + * The project a `branches` command acts on: the flag, else the linked parent, because branch + * management endpoints answer 403 for a branch's own ref. + */ +export const resolveParentScopedProjectRef = Effect.fn("ParentProjectRef.resolve")(function* ( + flagValue: Option.Option, +) { + const resolver = yield* ProjectRefResolver; + + if (Option.isSome(flagValue) && flagValue.value.length > 0) { + yield* Effect.annotateCurrentSpan("parent_project_ref.source", "flag"); + return yield* resolver.resolve(flagValue); + } + + const parent = yield* resolveLinkedParentRef(); + yield* Effect.annotateCurrentSpan("parent_project_ref.linked_parent", parent.kind); + if (parent.kind === "resolved") { + yield* Effect.annotateCurrentSpan("parent_project_ref.source", "linked_parent"); + const ref = yield* checkedParentRef(parent); + const cliSettings = yield* CommandSettings; + const cache = yield* readCachedLinkedProject( + yield* FileSystem.FileSystem, + yield* Path.Path, + cliSettings.workdir, + ); + yield* announceTarget(describeLinkedTarget({ ref, cache })); + return ref; + } + + yield* Effect.annotateCurrentSpan("parent_project_ref.source", "resolver"); + return yield* resolver.resolve(Option.none()); +}); diff --git a/apps/cli/src/command-internal/target/remote-resolution.ts b/apps/cli/src/command-internal/target/remote-resolution.ts new file mode 100644 index 0000000000..3d83077875 --- /dev/null +++ b/apps/cli/src/command-internal/target/remote-resolution.ts @@ -0,0 +1,308 @@ +import { Effect, FileSystem, Option, Path, Result } from "effect"; + +import { AccessTokenRequiredError, InvalidAccessTokenError } from "../../auth/errors.ts"; +import { CommandSettings } from "../../config/command-settings.service.ts"; +import { ProjectRefResolver, type ResolvedProjectRef } from "../../config/project-ref.service.ts"; +import { CliArgs } from "../../shared/cli/cli-args.service.ts"; +import { CommandTarget } from "../../shared/output/command-target.service.ts"; +import { Output } from "../../shared/output/output.service.ts"; +import { + CommandRuntime, + getCommandRuntimeCommand, +} from "../../shared/runtime/command-runtime.service.ts"; +import { yellow } from "../colors.ts"; +import { formatNamedRef, sanitizeInlineName } from "../http-errors.ts"; +import { readLinkSnapshotSoft } from "../../shared/config/link-snapshot.ts"; +import { readCachedLinkedProject, resolveLinkedParentRef } from "../parent-project-ref.ts"; +import { BRANCH_PROJECT_REF_PATTERN } from "../ref-patterns.ts"; +import { + findBranch, + isDefaultBranch, + listBranchesFailureCause, + listLinkedParentBranches, + previewBranchHint, + resolveRemoteBranchRef, +} from "./branch-resolution.ts"; +import { + LinkBranchListNetworkError, + LinkBranchListStatusError, +} from "./branch-resolution.errors.ts"; +import { nearMissByName } from "./closest-branch.ts"; +import type { RemoteValue } from "./remote-value.ts"; +import { stripUserinfo } from "./remote-vocabulary.ts"; +import type { LinkedTarget } from "./resolve-target.ts"; +import { deprecationMessages, remoteFlagText, shellQuote } from "./target-deprecation.ts"; +import type { TargetFlagValues } from "./target-flags.ts"; +import { describeLinkedTarget, renderBranchTarget } from "./target-identity.ts"; +import { parseTargetSelector, type TargetDeprecation } from "./target-selector.ts"; +import { TargetRemoteAmbiguousError } from "./target.errors.ts"; + +/** A `--remote` value that names a hosted project, as opposed to a database URL. */ +export type HostedRemote = Exclude; + +/** Prints a target warning on stderr in every output format, since stdout may carry the payload. */ +export const warnTarget = Effect.fnUntraced(function* (message: string) { + const output = yield* Output; + yield* output.raw(`${yellow(`Warning: ${message}`)}\n`, "stderr"); +}); + +/** Prints one warning per deprecated selector flag, naming its `--remote` replacement. */ +const warnTargetDeprecations = Effect.fnUntraced(function* ( + deprecations: ReadonlyArray, +) { + if (deprecations.length === 0) return; + const { projectHost } = yield* CommandSettings; + for (const message of deprecationMessages(deprecations, projectHost)) { + yield* warnTarget(message); + } +}); + +/** + * Parses the selector flags, prints the deprecation warnings, and names the command for the + * errors that need it. + */ +export const selectTarget = Effect.fnUntraced(function* ( + flags: TargetFlagValues, + options: { readonly valueHint?: string } = {}, +) { + const runtime = yield* Effect.serviceOption(CommandRuntime); + const commandName = Option.isSome(runtime) + ? `\`supabase ${getCommandRuntimeCommand(runtime.value)}\`` + : "This command"; + const cliArgs = yield* Effect.serviceOption(CliArgs); + const remoteConflict = Option.getOrUndefined(cliArgs)?.remoteConflict === true; + const { selection, deprecations } = yield* Result.match( + parseTargetSelector(flags, { remoteConflict, ...options }), + { onFailure: Effect.fail, onSuccess: Effect.succeed }, + ); + yield* warnTargetDeprecations(deprecations); + return { selection, commandName }; +}); + +const linkedProjectName = Effect.fnUntraced(function* (ref: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const { workdir } = yield* CommandSettings; + const cache = Option.getOrUndefined(yield* readCachedLinkedProject(fs, path, workdir)); + return cache?.ref === ref ? cache.name : undefined; +}); + +const LOGIN_SUGGESTION = + "Run `supabase login` with an account that can list this project's branches."; +const SET_TOKEN_SUGGESTION = "Set SUPABASE_ACCESS_TOKEN to a valid access token."; +const FORBIDDEN_SUGGESTION = "Use an access token that can read this project's branches."; + +type TokenSource = "env" | "stored"; + +interface ListingFailure { + readonly reason: string; + readonly suggestion?: string | undefined; + readonly status?: number; + readonly authFailure?: "login" | "token"; +} + +const statusFailure = ( + status: number, + parentRef: string, + tokenSource: TokenSource, +): ListingFailure => { + const reason = `: HTTP ${status}`; + switch (status) { + case 401: + return tokenSource === "env" + ? { status, reason, suggestion: SET_TOKEN_SUGGESTION, authFailure: "token" } + : { status, reason, suggestion: LOGIN_SUGGESTION }; + case 403: + return { status, reason, suggestion: FORBIDDEN_SUGGESTION }; + case 404: + return { status, reason, suggestion: previewBranchHint(parentRef) }; + default: + return { status, reason }; + } +}; + +const listingFailure = ( + error: unknown, + parentRef: string, + tokenSource: TokenSource, +): ListingFailure => { + if (error instanceof LinkBranchListStatusError) { + return statusFailure(error.status, parentRef, tokenSource); + } + if (error instanceof LinkBranchListNetworkError) { + return { reason: `: ${sanitizeInlineName(stripUserinfo(listBranchesFailureCause(error)))}` }; + } + if (error instanceof AccessTokenRequiredError) { + return { + reason: " because no access token is set", + suggestion: LOGIN_SUGGESTION, + authFailure: "login", + }; + } + if (error instanceof InvalidAccessTokenError) { + return error.source === "env" + ? { + reason: " because SUPABASE_ACCESS_TOKEN is not a valid access token", + suggestion: SET_TOKEN_SUGGESTION, + authFailure: "token", + } + : { + reason: " because the stored access token is not valid", + suggestion: LOGIN_SUGGESTION, + authFailure: "login", + }; + } + return { reason: "" }; +}; + +const commandUsage = Effect.fnUntraced(function* () { + const runtime = yield* Effect.serviceOption(CommandRuntime); + return `supabase ${Option.isSome(runtime) ? getCommandRuntimeCommand(runtime.value) : ""}`; +}); + +const checkRemoteOperand = Effect.fn("Target.checkRemoteOperand")(function* (rawOperand: string) { + const parent = yield* resolveLinkedParentRef(); + if (parent.kind !== "resolved") return; + + const operand = stripUserinfo(rawOperand); + const usage = yield* commandUsage(); + const options = `For the branch: ${usage} ${remoteFlagText({ _tag: "Branch", name: operand })}. For the linked project: ${usage} ${shellQuote(operand)} --remote`; + + const listed = yield* listLinkedParentBranches(parent, "Checking branches...").pipe( + Effect.result, + ); + if (Result.isFailure(listed)) { + const { accessToken } = yield* CommandSettings; + const failure = listingFailure( + listed.failure, + parent.ref, + Option.isSome(accessToken) ? "env" : "stored", + ); + return yield* new TargetRemoteAmbiguousError({ + message: `Can't tell whether the word after --remote is a branch (listing the branches of project ${parent.ref} failed${failure.reason}). For the branch: ${usage} --remote=. For the linked project, put --remote last: ${usage} --remote`, + ...(failure.suggestion === undefined ? {} : { suggestion: failure.suggestion }), + ...(failure.status === undefined ? {} : { status: failure.status }), + ...(failure.authFailure === undefined ? {} : { authFailure: failure.authFailure }), + cause: listed.failure, + }); + } + const { parentRef, branches } = listed.success; + if (findBranch(branches, operand) !== undefined) { + const name = yield* linkedProjectName(parentRef); + const project = name === undefined ? `project ${parentRef}` : formatNamedRef(name, parentRef); + return yield* new TargetRemoteAmbiguousError({ + message: `"${operand}" is a branch of ${project}. ${options}`, + }); + } + const closest = nearMissByName(branches, operand, (branch) => isDefaultBranch(branch, parentRef)); + if (closest !== undefined) { + const closestName = sanitizeInlineName(closest.name); + return yield* new TargetRemoteAmbiguousError({ + message: `"${operand}" isn't a branch, but "${closestName}" is. For the branch: ${usage} ${remoteFlagText({ _tag: "Branch", name: closestName })}. For the linked project: ${usage} ${shellQuote(operand)} --remote`, + }); + } +}); + +const rejectRefShapedOperand = Effect.fnUntraced(function* (operand: string) { + const usage = yield* commandUsage(); + return yield* new TargetRemoteAmbiguousError({ + message: `"${operand}" after --remote looks like a project ref. For that project: ${usage} --remote=${operand}. To keep it as an operand, put --remote last: ${usage} --remote`, + }); +}); + +const bareRemoteWord = Effect.fnUntraced(function* () { + const cliArgs = yield* Effect.serviceOption(CliArgs); + return Option.getOrUndefined(cliArgs)?.bareRemoteOperand; +}); + +/** Fails a bare `--remote` followed by a project ref, before any prompt, connection or write. */ +export const rejectRefShapedRemote = Effect.fnUntraced(function* () { + const operand = yield* bareRemoteWord(); + if (operand !== undefined && BRANCH_PROJECT_REF_PATTERN.test(operand)) { + return yield* rejectRefShapedOperand(operand); + } +}); + +/** + * Fails a bare `--remote` whose next word is a project ref, or is, or may be, a branch of the + * linked project, before the command prompts, connects or writes. + */ +const rejectAmbiguousRemote = Effect.fnUntraced(function* () { + yield* rejectRefShapedRemote(); + const operand = yield* bareRemoteWord(); + if (operand !== undefined) yield* checkRemoteOperand(operand); +}); + +/** The linked target for a ref the ref chain has already resolved and announced. */ +const linkedTarget = (resolved: ResolvedProjectRef): LinkedTarget => ({ + kind: "linked", + ref: resolved.ref, + source: resolved.source, + matchesLinkedRef: resolved.matchesLinkedRef, + ...(resolved.poolerUrl === undefined ? {} : { poolerUrl: resolved.poolerUrl }), +}); + +const resolveRemoteBranch = Effect.fnUntraced(function* (name: string) { + const found = yield* resolveRemoteBranchRef(name); + const cell = yield* Effect.serviceOption(CommandTarget); + if (Option.isSome(cell)) { + if (found.isDefault === undefined) { + yield* cell.value.announce( + describeLinkedTarget({ ref: found.projectRef, cache: Option.none() }), + ); + return found; + } + const nameOwner = found.isDefault ? found.projectRef : found.parentRef; + yield* cell.value.announce( + renderBranchTarget({ + branch: Option.fromNullishOr(found.branchName), + ref: found.projectRef, + parentRef: Option.fromNullishOr(found.parentRef), + parentName: nameOwner === undefined ? undefined : yield* linkedProjectName(nameOwner), + isDefault: found.isDefault, + }), + ); + } + return found; +}); + +/** The linked target for one hosted `--remote` value, announced as a branch when it names one. */ +export const resolveRemoteTarget = Effect.fn("Target.resolveRemote")(function* ( + remote: HostedRemote, +) { + if (remote._tag === "Branch") { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const { workdir } = yield* CommandSettings; + const found = yield* resolveRemoteBranch(remote.name); + const link = yield* readLinkSnapshotSoft(fs, path, workdir); + const matchesLinkedRef = Option.contains(link.ref, found.projectRef); + const poolerUrl = matchesLinkedRef + ? Option.getOrUndefined(Option.filter(link.poolerUrl, (url) => url.length > 0)) + : undefined; + return { + ...linkedTarget({ + ref: found.projectRef, + source: "flag", + matchesLinkedRef, + ...(poolerUrl === undefined ? {} : { poolerUrl }), + }), + branch: remote.name, + ...(found.parentRef === undefined ? {} : { parentRef: found.parentRef }), + ...(found.isDefault === undefined ? {} : { isDefault: found.isDefault }), + } satisfies LinkedTarget; + } + const projectRef = yield* ProjectRefResolver; + return linkedTarget( + yield* projectRef.resolveRef(remote._tag === "Ref" ? Option.some(remote.ref) : Option.none()), + ); +}); + +/** + * Resolves a hosted selection to its project: a bare `--remote` first checks that the word after + * it does not name a branch, then the value's own lookup runs. + */ +export const resolveHostedRemote = Effect.fnUntraced(function* (remote: HostedRemote) { + if (remote._tag === "Linked") yield* rejectAmbiguousRemote(); + return yield* resolveRemoteTarget(remote); +}); diff --git a/apps/cli/src/command-internal/target/remote-resolution.unit.test.ts b/apps/cli/src/command-internal/target/remote-resolution.unit.test.ts new file mode 100644 index 0000000000..524a7078b7 --- /dev/null +++ b/apps/cli/src/command-internal/target/remote-resolution.unit.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it } from "@effect/vitest"; +import { Effect, Layer, Option, Sink, Stdio, Stream } from "effect"; +import { vi } from "vitest"; + +import { stripAnsi } from "../../../tests/helpers/ansi.ts"; +import { mockCommandSettings } from "../../../tests/helpers/command-mocks.ts"; +import { mockTty } from "../../../tests/helpers/mocks.ts"; +import { + jsonOutputLayer, + streamJsonOutputLayer, + textOutputLayer, +} from "../../shared/output/output.layer.ts"; +import { Output } from "../../shared/output/output.service.ts"; +import type { Tty } from "../../shared/runtime/tty.service.ts"; +import { selectTarget } from "./remote-resolution.ts"; +import type { TargetFlagValues } from "./target-flags.ts"; + +const clack = vi.hoisted(() => ({ + log: { message: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), step: vi.fn() }, +})); + +vi.mock("@clack/prompts", () => ({ + log: clack.log, + intro: vi.fn(), + outro: vi.fn(), + cancel: vi.fn(), + isCancel: vi.fn(() => false), + spinner: vi.fn(), +})); + +const sink = (chunks: Array) => + Sink.forEach((item: string | Uint8Array) => + Effect.sync(() => { + chunks.push(typeof item === "string" ? item : new TextDecoder().decode(item)); + }), + ); + +const PAYLOAD = "export type Database = {}\n"; +const flags = (overrides: Partial): TargetFlagValues => ({ + remote: Option.none(), + local: false, + linked: false, + projectRef: Option.none(), + ...overrides, +}); + +const runWith = ( + outputLayer: Layer.Layer, + selector: TargetFlagValues, +) => { + const stdout: Array = []; + const stderr: Array = []; + const stdio = Layer.succeed( + Stdio.Stdio, + Stdio.make({ + args: Effect.succeed([]), + stdin: Stream.empty, + stdout: () => sink(stdout), + stderr: () => sink(stderr), + }), + ); + return Effect.gen(function* () { + yield* selectTarget(selector); + const output = yield* Output; + yield* output.raw(PAYLOAD); + }).pipe( + Effect.provide( + Layer.mergeAll(outputLayer, mockCommandSettings({ workdir: "/unused" })).pipe( + Layer.provide(Layer.merge(stdio, mockTty())), + ), + ), + Effect.map(() => ({ stdout: stdout.join(""), stderr: stderr.join("") })), + ); +}; + +describe("selectTarget deprecation warnings", () => { + it.effect.each([ + { format: "text", layer: textOutputLayer }, + { format: "json", layer: jsonOutputLayer }, + { format: "stream-json", layer: streamJsonOutputLayer }, + ])("leaves stdout to the payload in $format mode", ({ layer }) => + Effect.gen(function* () { + const remoteRun = yield* runWith(layer, flags({ remote: Option.some({ _tag: "Linked" }) })); + const aliasRun = yield* runWith(layer, flags({ linked: true })); + + expect(aliasRun.stdout).toBe(PAYLOAD); + expect(aliasRun.stdout).toBe(remoteRun.stdout); + expect(remoteRun.stderr).toBe(""); + expect(stripAnsi(aliasRun.stderr)).toBe( + "Warning: --linked is deprecated and will be removed in v4. Use --remote instead\n", + ); + expect(clack.log.warn).not.toHaveBeenCalled(); + }), + ); +}); diff --git a/apps/cli/src/command-internal/target/remote-value.ts b/apps/cli/src/command-internal/target/remote-value.ts new file mode 100644 index 0000000000..dc022ff778 --- /dev/null +++ b/apps/cli/src/command-internal/target/remote-value.ts @@ -0,0 +1,83 @@ +import { Result } from "effect"; + +import { BRANCH_PROJECT_REF_PATTERN } from "../ref-patterns.ts"; +import { + DB_URL_PATTERN, + KEYWORD_DSN_PATTERN, + LINKED_LITERAL, + OFF_LITERAL, +} from "./remote-vocabulary.ts"; + +/** What `--remote` (or a `db diff --from/--to` value) names. */ +export type RemoteValue = + | { readonly _tag: "Linked" } + | { readonly _tag: "Ref"; readonly ref: string } + | { readonly _tag: "DbUrl"; readonly url: string } + | { readonly _tag: "Branch"; readonly name: string }; + +export type NamedRemoteValue = Exclude; + +/** A `://` value that is not a postgres URL; `ref` is set when it is a project's own URL. */ +interface ForeignUrlRejection { + readonly _tag: "ForeignUrl"; + readonly ref: string | undefined; +} + +/** A `host=… user=…` connection string, which `--remote` does not read as a branch name. */ +interface KeywordDsnRejection { + readonly _tag: "KeywordDsn"; +} + +export type NamedRemoteRejection = ForeignUrlRejection | KeywordDsnRejection; + +export type RemoteValueRejection = + | { readonly _tag: "Empty" } + | { readonly _tag: "Off" } + | NamedRemoteRejection; + +const PROJECT_URL_REF = /^[a-z][a-z0-9+.-]*:\/\/([a-z]{20})\.supabase\.[a-z]+(?::\d+)?(?:[/?#]|$)/; + +const refOfProjectUrl = (value: string): string | undefined => + PROJECT_URL_REF.exec(value.toLowerCase())?.[1]; + +/** A project ref, a postgres URL or a branch name; any other URL or keyword string is rejected. */ +export const parseNamedRemote = ( + raw: string, +): Result.Result => { + const value = raw.trim(); + if (BRANCH_PROJECT_REF_PATTERN.test(value)) return Result.succeed({ _tag: "Ref", ref: value }); + if (DB_URL_PATTERN.test(value)) { + return Result.succeed({ + _tag: "DbUrl", + url: value.replace(DB_URL_PATTERN, (scheme) => scheme.toLowerCase()), + }); + } + if (value.includes("://")) { + return Result.fail({ _tag: "ForeignUrl", ref: refOfProjectUrl(value) }); + } + if (KEYWORD_DSN_PATTERN.test(value)) return Result.fail({ _tag: "KeywordDsn" }); + return Result.succeed({ _tag: "Branch", name: value }); +}; + +/** Classifies a `--remote` value, or why it is rejected: empty, off, a foreign URL or a DSN. */ +export const parseRemoteValue = (raw: string): Result.Result => { + const value = raw.trim(); + if (LINKED_LITERAL.test(value)) return Result.succeed({ _tag: "Linked" }); + if (value === "") return Result.fail({ _tag: "Empty" }); + if (OFF_LITERAL.test(value)) return Result.fail({ _tag: "Off" }); + return parseNamedRemote(value); +}; + +/** {@link parseNamedRemote} for an accepted flag value; a rejection reads as a branch. */ +const classifyNamedRemote = (raw: string): NamedRemoteValue => + Result.getOrElse(parseNamedRemote(raw), (): NamedRemoteValue => ({ + _tag: "Branch", + name: raw.trim(), + })); + +/** {@link parseRemoteValue} for the same caller; `undefined` for an empty or off value. */ +export const classifyRemoteValue = (raw: string): RemoteValue | undefined => { + const value = raw.trim(); + if (value === "" || OFF_LITERAL.test(value)) return undefined; + return Result.getOrUndefined(parseRemoteValue(value)) ?? classifyNamedRemote(value); +}; diff --git a/apps/cli/src/command-internal/target/remote-value.unit.test.ts b/apps/cli/src/command-internal/target/remote-value.unit.test.ts new file mode 100644 index 0000000000..64e50fc21f --- /dev/null +++ b/apps/cli/src/command-internal/target/remote-value.unit.test.ts @@ -0,0 +1,90 @@ +import { Result } from "effect"; +import { describe, expect, it } from "vitest"; + +import { parseNamedRemote, parseRemoteValue } from "./remote-value.ts"; +import { couldNameBranch, stripUserinfo } from "./remote-vocabulary.ts"; + +const REF = "abcdefghijklmnopqrst"; + +describe("parseRemoteValue", () => { + it.each([ + { raw: " true ", expected: { _tag: "Linked" } }, + { raw: "YES", expected: { _tag: "Linked" } }, + { raw: ` ${REF} `, expected: { _tag: "Ref", ref: REF } }, + { raw: "Postgres://h/db", expected: { _tag: "DbUrl", url: "postgres://h/db" } }, + { raw: " POSTGRESQL://u@h/db", expected: { _tag: "DbUrl", url: "postgresql://u@h/db" } }, + { raw: " staging ", expected: { _tag: "Branch", name: "staging" } }, + ])("reads $raw as $expected", ({ raw, expected }) => { + expect(parseRemoteValue(raw)).toEqual(Result.succeed(expected)); + }); + + it.each([ + { raw: "", expected: { _tag: "Empty" } }, + { raw: " ", expected: { _tag: "Empty" } }, + { raw: "False", expected: { _tag: "Off" } }, + { raw: " 0 ", expected: { _tag: "Off" } }, + { raw: `https://${REF}.supabase.co`, expected: { _tag: "ForeignUrl", ref: REF } }, + { raw: `HTTPS://${REF}.SUPABASE.CO:443/x`, expected: { _tag: "ForeignUrl", ref: REF } }, + { raw: `https://${REF}.supabase.com?x=1`, expected: { _tag: "ForeignUrl", ref: REF } }, + { raw: "https://db.supabase.co", expected: { _tag: "ForeignUrl", ref: undefined } }, + { raw: "ftp://example.com", expected: { _tag: "ForeignUrl", ref: undefined } }, + { raw: "host=db.example.com user=postgres", expected: { _tag: "KeywordDsn" } }, + { raw: "dbname=postgres", expected: { _tag: "KeywordDsn" } }, + { raw: "my branch", expected: { _tag: "KeywordDsn" } }, + ])("rejects $raw as $expected", ({ raw, expected }) => { + expect(parseRemoteValue(raw)).toEqual(Result.fail(expected)); + }); + + it.each(["y", "Y"])("reads %s as the linked project, not a branch", (raw) => { + expect(parseRemoteValue(raw)).toEqual(Result.succeed({ _tag: "Linked" })); + }); + + it.each(["n", "N"])("rejects %s as an off switch, not a branch", (raw) => { + expect(parseRemoteValue(raw)).toEqual(Result.fail({ _tag: "Off" })); + }); +}); + +describe("parseNamedRemote", () => { + it.each(["true", "false", "False", "YES", "off", "1", "0"])( + "treats the boolean literal %s as a branch name", + (name) => { + expect(parseNamedRemote(name)).toEqual(Result.succeed({ _tag: "Branch", name })); + }, + ); +}); + +describe("couldNameBranch", () => { + it.each(["feature-x", "feature/x", "v1.2"])("reads %s as a branch name", (word) => { + expect(couldNameBranch(word)).toBe(true); + }); + + it.each([ + "", + "STRIPE_KEY=sk_live_123", + "host=db.example.com", + "two words", + "ss:///bucket", + "123", + "./path", + ])("reads %j as something else", (word) => { + expect(couldNameBranch(word)).toBe(false); + }); +}); + +describe("stripUserinfo", () => { + it.each([ + { value: "postgresql://u:pw@h/db", expected: "postgresql://h/db" }, + { value: "postgresql://u@h/db", expected: "postgresql://h/db" }, + { value: "u:pw@staging", expected: "staging" }, + { value: "u:p@ss@h", expected: "h" }, + { value: "postgresql://u:p@ss@h/db", expected: "postgresql://h/db" }, + { value: "postgresql://u:pw@h/db?x=a@b", expected: "postgresql://h/db?x=a@b" }, + { value: "staging", expected: "staging" }, + { + value: "a message with postgresql://u:pw@h and more", + expected: "a message with postgresql://h and more", + }, + ])("turns $value into $expected", ({ value, expected }) => { + expect(stripUserinfo(value)).toBe(expected); + }); +}); diff --git a/apps/cli/src/command-internal/target/remote-vocabulary.ts b/apps/cli/src/command-internal/target/remote-vocabulary.ts new file mode 100644 index 0000000000..5c41bc7158 --- /dev/null +++ b/apps/cli/src/command-internal/target/remote-vocabulary.ts @@ -0,0 +1,33 @@ +export const REMOTE_FLAG_NAME = "remote"; + +export const DB_URL_PATTERN = /^postgres(ql)?:\/\//i; + +/** The spellings of a bare `--remote`, in any case: the words the flag parser reads as true. */ +export const LINKED_LITERAL = /^(true|yes|on|1|y)$/i; + +/** The lowercase words the flag parser reads as false. */ +export const OFF_WORDS: ReadonlySet = new Set(["false", "no", "off", "0", "n"]); + +/** {@link OFF_WORDS} in any case. */ +export const OFF_LITERAL = new RegExp(`^(${[...OFF_WORDS].join("|")})$`, "i"); + +export const isBooleanLiteral = (word: string): boolean => + LINKED_LITERAL.test(word) || OFF_LITERAL.test(word); + +/** Whitespace or `=`: a keyword connection string or `NAME=value` pair, never a branch name. */ +export const KEYWORD_DSN_PATTERN = /[\s=]/; + +const NOT_A_BRANCH_NAME: ReadonlyArray = [ + KEYWORD_DSN_PATTERN, + /^[a-z][a-z0-9+.-]*:\/\//i, + /^\d+$/, + /^[./~]/, +]; + +/** Whether a word reads as a branch name, not a path, URL, number, sentence or `NAME=value`. */ +export const couldNameBranch = (word: string): boolean => + word !== "" && !NOT_A_BRANCH_NAME.some((pattern) => pattern.test(word)); + +/** Removes `user:pass@` from a value before it appears in an error or a warning. */ +export const stripUserinfo = (value: string): string => + value.replace(/\/\/[^\s/]*@/g, "//").replace(/[^\s/@:]+:[^\s/]*@/g, ""); diff --git a/apps/cli/src/command-internal/target/resolve-project-target.integration.test.ts b/apps/cli/src/command-internal/target/resolve-project-target.integration.test.ts new file mode 100644 index 0000000000..389637ca53 --- /dev/null +++ b/apps/cli/src/command-internal/target/resolve-project-target.integration.test.ts @@ -0,0 +1,475 @@ +import { BunServices } from "@effect/platform-bun"; +import { describe, expect, it } from "@effect/vitest"; +import { Cause, Effect, Exit, FileSystem, Layer, Option, Path } from "effect"; + +import { + jsonResponse, + mockCommandPlatformApi, + mockCommandSettings, +} from "../../../tests/helpers/command-mocks.ts"; +import { stripAnsi } from "../../../tests/helpers/ansi.ts"; +import { mockOutput } from "../../../tests/helpers/mocks.ts"; +import { branchRow } from "../../../tests/helpers/remote-selection.ts"; +import { ProjectRefNotLinkedError } from "../../config/project-ref.errors.ts"; +import { ProjectRefResolver } from "../../config/project-ref.service.ts"; +import { rootCommandForFeatures } from "../../cli/root.ts"; +import { CliArgs } from "../../shared/cli/cli-args.service.ts"; +import { prepareCliArgs } from "../../shared/cli/run.ts"; +import { commandTargetLayer } from "../../shared/output/command-target.layer.ts"; +import { + CommandTarget, + type CommandTargetJson, +} from "../../shared/output/command-target.service.ts"; +import { commandRuntimeLayer } from "../../shared/runtime/command-runtime.layer.ts"; +import type { RemoteValue } from "./remote-value.ts"; +import { resolveProjectTarget } from "./resolve-project-target.ts"; +import type { ProjectTargetFlagValues } from "./target-flags.ts"; + +const FILE_REF = "filefilefilefilefile"; +const FLAG_REF = "flagflagflagflagflag"; +const BRANCH_REF = "branchprojectrefabcd"; + +const fileBranch = (name: string, overrides: Parameters[1] = {}) => + branchRow(name, { parent_project_ref: FILE_REF, ...overrides }); + +const BRANCH_UUID = "11111111-2222-4333-8444-555555555555"; + +const branchConfig = (ref: string, status: string) => ({ + ref, + status, + postgres_version: "15", + postgres_engine: "15", + release_channel: "ga", + db_host: "h", + db_port: 5432, +}); + +const root = rootCommandForFeatures(); + +interface RunOptions { + readonly linked?: boolean; + readonly savedRef?: string; + readonly args?: ReadonlyArray; + readonly branches?: ReadonlyArray>; + readonly branchById?: { readonly status: number; readonly body: unknown }; + readonly interactive?: boolean; +} + +interface RunInput { + readonly remote?: RemoteValue; + readonly projectRef?: string; +} + +const runWith = ( + input: RunInput, + options: RunOptions, + resolve: (flags: ProjectTargetFlagValues) => Effect.Effect, +) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = yield* fs.makeTempDirectoryScoped({ prefix: "resolve-project-target-" }); + if (options.savedRef !== undefined) { + yield* fs.makeDirectory(path.join(dir, "supabase", ".temp"), { recursive: true }); + yield* fs.writeFileString( + path.join(dir, "supabase", ".temp", "project-ref"), + options.savedRef, + ); + } + const refFlags: Array> = []; + const resolver = Layer.succeed(ProjectRefResolver, { + resolveRef: (flagValue) => + options.linked === false + ? Effect.fail(new ProjectRefNotLinkedError({ message: "Cannot find project ref." })) + : Effect.sync(() => { + refFlags.push(flagValue); + return { + ref: Option.getOrElse(flagValue, () => FILE_REF), + source: Option.isSome(flagValue) ? ("flag" as const) : ("file" as const), + matchesLinkedRef: Option.isNone(flagValue), + }; + }), + resolve: () => Effect.die("resolve is not used by resolveProjectTarget"), + resolveForLink: () => Effect.die("resolveForLink is not used by resolveProjectTarget"), + resolveOptional: () => Effect.die("resolveOptional is not used by resolveProjectTarget"), + promptProjectRef: () => Effect.die("promptProjectRef is not used by resolveProjectTarget"), + }); + const out = mockOutput( + options.interactive === undefined ? {} : { interactive: options.interactive }, + ); + const api = mockCommandPlatformApi({ + handler: (request) => { + if (request.url.includes("/v1/branches/")) { + const byId = options.branchById ?? { status: 404, body: { message: "not found" } }; + return Effect.succeed(jsonResponse(request, byId.status, byId.body)); + } + return Effect.succeed(jsonResponse(request, 200, options.branches ?? [])); + }, + }); + let announced: Option.Option = Option.none(); + const exit = yield* resolve({ + remote: Option.fromNullishOr(input.remote), + projectRef: Option.fromNullishOr(input.projectRef), + }).pipe( + Effect.tap(() => + CommandTarget.use((cell) => cell.json).pipe( + Effect.tap((json) => + Effect.sync(() => { + announced = json; + }), + ), + ), + ), + Effect.provide( + Layer.mergeAll( + commandTargetLayer.pipe(Layer.provide(out.layer)), + out.layer, + resolver, + api.layer, + mockCommandSettings({ workdir: dir, projectId: Option.none() }), + commandRuntimeLayer(["functions", "deploy"]).pipe(Layer.provide(BunServices.layer)), + options.args === undefined + ? Layer.empty + : Layer.succeed(CliArgs, prepareCliArgs(root, options.args)), + ), + ), + Effect.exit, + ); + return { + exit, + stderr: out.stderrText, + stdout: out.stdoutText, + target: announced, + warnings: stripAnsi(out.stderrText) + .split("\n") + .filter((line) => line.startsWith("Warning: ")) + .map((line) => line.slice("Warning: ".length)), + outputWarnCalls: out.messages.filter((m) => m.type === "warn"), + progressEvents: out.progressEvents, + refFlags, + branchRequests: api.requests.length, + requestedUrls: api.requests.map((request) => request.url), + }; + }).pipe(Effect.scoped, Effect.provide(BunServices.layer)); + +const run = (input: RunInput, options: RunOptions = {}) => + runWith(input, options, (flags) => resolveProjectTarget(flags)); + +const runDetailed = (input: RunInput, options: RunOptions = {}) => + runWith(input, options, (flags) => resolveProjectTarget(flags, { detail: true })); + +const successOf = (exit: Exit.Exit): A => { + if (!Exit.isSuccess(exit)) throw new Error(`expected success, got ${Cause.pretty(exit.cause)}`); + return exit.value; +}; + +const refOf = (exit: Exit.Exit) => successOf(exit); + +const failureOf = (exit: Exit.Exit) => + Exit.isFailure(exit) ? Cause.squash(exit.cause) : undefined; + +describe("resolveProjectTarget", () => { + it.effect.each([ + { name: "no selector", input: {} }, + { name: "a bare --remote", input: { remote: { _tag: "Linked" } as const } }, + ])("falls through to the project-ref chain for $name", ({ input }) => + Effect.gen(function* () { + const result = yield* run(input); + expect(refOf(result.exit)).toBe(FILE_REF); + expect(result.refFlags).toEqual([Option.none()]); + expect(result.warnings).toEqual([]); + }), + ); + + it.effect("fails before any lookup when --remote is given twice with different values", () => + Effect.gen(function* () { + const result = yield* run( + { remote: { _tag: "Branch", name: "feature-x" } }, + { + savedRef: FILE_REF, + args: ["functions", "deploy", "--remote=feature-x", `--remote=${FLAG_REF}`], + branches: [fileBranch("feature-x")], + }, + ); + expect(failureOf(result.exit)).toMatchObject({ + _tag: "TargetSelectorConflictError", + flags: ["--remote"], + message: "--remote is given more than once with different values; pass it once.", + suggestion: "Pass --remote once, as --remote or --remote=.", + }); + expect(result.branchRequests).toBe(0); + expect(result.refFlags).toEqual([]); + }), + ); + + it.effect("uses the project --remote= names, with no deprecation warning", () => + Effect.gen(function* () { + const result = yield* run({ remote: { _tag: "Ref", ref: FLAG_REF } }); + expect(refOf(result.exit)).toBe(FLAG_REF); + expect(result.refFlags).toEqual([Option.some(FLAG_REF)]); + expect(result.warnings).toEqual([]); + expect(result.branchRequests).toBe(0); + }), + ); + + it.effect("keeps --project-ref working and warns with the --remote form of the command", () => + Effect.gen(function* () { + const result = yield* run( + { projectRef: FLAG_REF }, + { args: ["functions", "deploy", "--project-ref", FLAG_REF, "hello"] }, + ); + expect(refOf(result.exit)).toBe(FLAG_REF); + expect(result.warnings).toEqual([ + `--project-ref is deprecated and will be removed in v4. Use --remote= instead`, + ]); + expect(result.outputWarnCalls).toEqual([]); + expect(result.stdout).toBe(""); + }), + ); + + it.effect("looks a branch name up under the linked project and announces the branch", () => + Effect.gen(function* () { + const result = yield* run( + { remote: { _tag: "Branch", name: "feature-x" } }, + { savedRef: FILE_REF, branches: [fileBranch("feature-x")] }, + ); + expect(refOf(result.exit)).toBe(BRANCH_REF); + expect(result.branchRequests).toBe(1); + expect(result.refFlags).toEqual([]); + expect(result.stderr).toContain(`project ${FILE_REF} · branch feature-x (${BRANCH_REF})`); + }), + ); + + it.effect("shows the branch lookup spinner on a terminal", () => + Effect.gen(function* () { + const result = yield* run( + { remote: { _tag: "Branch", name: "feature-x" } }, + { savedRef: FILE_REF, branches: [fileBranch("feature-x")], interactive: true }, + ); + expect(refOf(result.exit)).toBe(BRANCH_REF); + expect(result.progressEvents).toContainEqual({ + type: "start", + message: "Resolving branch...", + }); + }), + ); + + it.effect("keeps the branch lookup off stdout when it is not a terminal", () => + Effect.gen(function* () { + const result = yield* run( + { remote: { _tag: "Branch", name: "feature-x" } }, + { savedRef: FILE_REF, branches: [fileBranch("feature-x")], interactive: false }, + ); + expect(refOf(result.exit)).toBe(BRANCH_REF); + expect(result.progressEvents).toEqual([]); + expect(result.stdout).toBe(""); + }), + ); + + it.effect("treats a --project-ref that is not a ref as a branch name", () => + Effect.gen(function* () { + const result = yield* run( + { projectRef: "feature-x" }, + { savedRef: FILE_REF, branches: [fileBranch("feature-x")] }, + ); + expect(refOf(result.exit)).toBe(BRANCH_REF); + }), + ); + + it.effect("rejects a database URL given to --project-ref, since a project has no database", () => + Effect.gen(function* () { + const result = yield* run({ + projectRef: "postgresql://postgres:pw@db.example.com:5432/postgres", + }); + expect(failureOf(result.exit)).toMatchObject({ + _tag: "TargetInvalidValueError", + flag: "--project-ref", + message: "--project-ref takes a project ref or a branch name, not a URL.", + }); + expect(result.refFlags).toEqual([]); + }), + ); + + it.effect("stops a bare --remote followed by a word that is a branch of the linked project", () => + Effect.gen(function* () { + const result = yield* run( + { remote: { _tag: "Linked" } }, + { + savedRef: FILE_REF, + args: ["functions", "deploy", "--remote", "feature-x"], + branches: [fileBranch("feature-x")], + }, + ); + expect(failureOf(result.exit)).toMatchObject({ + _tag: "TargetRemoteAmbiguousError", + message: `"feature-x" is a branch of project ${FILE_REF}. For the branch: supabase functions deploy --remote=feature-x. For the linked project: supabase functions deploy feature-x --remote`, + }); + }), + ); + + it.effect("carries the not-linked failure through instead of guessing a project", () => + Effect.gen(function* () { + const result = yield* run({}, { linked: false }); + expect(failureOf(result.exit)).toMatchObject({ _tag: "ProjectRefNotLinkedError" }); + }), + ); + + it.effect( + "reads the branch list once, matching a branch UUID by id under the linked project", + () => + Effect.gen(function* () { + const result = yield* run( + { remote: { _tag: "Branch", name: BRANCH_UUID } }, + { savedRef: FILE_REF, branches: [fileBranch("feature-x", { id: BRANCH_UUID })] }, + ); + expect(refOf(result.exit)).toBe(BRANCH_REF); + expect(result.requestedUrls).toEqual([ + `https://api.supabase.com/v1/projects/${FILE_REF}/branches`, + ]); + }), + ); + + it.effect( + "resolves a branch UUID through GET /v1/branches/{id} when no project is linked, as an unverified project", + () => + Effect.gen(function* () { + const result = yield* run( + { remote: { _tag: "Branch", name: BRANCH_UUID } }, + { branchById: { status: 200, body: branchConfig(BRANCH_REF, "ACTIVE_HEALTHY") } }, + ); + expect(refOf(result.exit)).toBe(BRANCH_REF); + expect(result.requestedUrls).toEqual([ + `https://api.supabase.com/v1/branches/${BRANCH_UUID}`, + ]); + expect(result.stderr).toBe(`Target: project ${BRANCH_REF}\n`); + expect(result.target).toEqual( + Option.some({ kind: "linked", project_ref: BRANCH_REF, environment: "unverified" }), + ); + }), + ); + + it.effect("reports an unknown branch UUID without a linked project", () => + Effect.gen(function* () { + const result = yield* run( + { remote: { _tag: "Branch", name: BRANCH_UUID } }, + { branchById: { status: 404, body: { message: "not found" } } }, + ); + expect(failureOf(result.exit)).toMatchObject({ + _tag: "LinkBranchListStatusError", + message: expect.stringContaining(`Branch "${BRANCH_UUID}" not found`), + }); + }), + ); + + it.effect("reports a UUID-shaped value the API would reject as an unknown branch", () => + Effect.gen(function* () { + const name = "11111111-2222-3333-4444-555555555555"; + const result = yield* run({ remote: { _tag: "Branch", name } }); + expect(failureOf(result.exit)).toMatchObject({ + _tag: "LinkBranchListStatusError", + status: 404, + message: `Branch "${name}" not found. Run \`supabase branches list\` to see available branches.`, + }); + expect(result.requestedUrls).toEqual([]); + }), + ); + + it.effect("reports a branch UUID whose project is still provisioning", () => + Effect.gen(function* () { + const result = yield* run( + { remote: { _tag: "Branch", name: BRANCH_UUID } }, + { branchById: { status: 200, body: branchConfig("", "CREATING_PROJECT") } }, + ); + expect(failureOf(result.exit)).toMatchObject({ + _tag: "LinkBranchNotReadyError", + message: expect.stringContaining("has no project ref yet"), + }); + }), + ); + + it.effect("asks for a linked project before looking a branch name up", () => + Effect.gen(function* () { + const result = yield* run({ remote: { _tag: "Branch", name: "feature-x" } }); + expect(failureOf(result.exit)).toMatchObject({ _tag: "LinkBranchNotLinkedError" }); + expect(result.branchRequests).toBe(0); + }), + ); +}); + +describe("resolveProjectTarget with detail", () => { + it.effect("carries the source and linked-ref match the ref chain resolved", () => + Effect.gen(function* () { + const flag = successOf((yield* runDetailed({ remote: { _tag: "Ref", ref: FLAG_REF } })).exit); + expect(flag).toEqual({ + kind: "linked", + ref: FLAG_REF, + source: "flag", + matchesLinkedRef: false, + }); + const file = successOf((yield* runDetailed({})).exit); + expect(file).toEqual({ + kind: "linked", + ref: FILE_REF, + source: "file", + matchesLinkedRef: true, + }); + }), + ); + + it.effect("names the branch and the project it was found under", () => + Effect.gen(function* () { + const result = yield* runDetailed( + { remote: { _tag: "Branch", name: "feature-x" } }, + { savedRef: FILE_REF, branches: [fileBranch("feature-x")] }, + ); + expect(successOf(result.exit)).toEqual({ + kind: "linked", + ref: BRANCH_REF, + source: "flag", + matchesLinkedRef: false, + branch: "feature-x", + parentRef: FILE_REF, + isDefault: false, + }); + }), + ); + + it.effect("marks the production branch as the default", () => + Effect.gen(function* () { + const result = yield* runDetailed( + { remote: { _tag: "Branch", name: "main" } }, + { + savedRef: FILE_REF, + branches: [fileBranch("main", { project_ref: FILE_REF, is_default: true })], + }, + ); + expect(successOf(result.exit)).toMatchObject({ ref: FILE_REF, isDefault: true }); + }), + ); + + it.effect("matches the linked ref when the branch's own ref is the saved one", () => + Effect.gen(function* () { + const result = yield* runDetailed( + { remote: { _tag: "Branch", name: "feature-x" } }, + { savedRef: BRANCH_REF, branches: [fileBranch("feature-x")] }, + ); + expect(successOf(result.exit)).toMatchObject({ ref: BRANCH_REF, matchesLinkedRef: true }); + }), + ); + + it.effect("names a branch resolved from a UUID by the UUID, with no parent", () => + Effect.gen(function* () { + const result = yield* runDetailed( + { remote: { _tag: "Branch", name: BRANCH_UUID } }, + { branchById: { status: 200, body: branchConfig(BRANCH_REF, "ACTIVE_HEALTHY") } }, + ); + expect(successOf(result.exit)).toEqual({ + kind: "linked", + ref: BRANCH_REF, + source: "flag", + matchesLinkedRef: false, + branch: BRANCH_UUID, + }); + }), + ); +}); diff --git a/apps/cli/src/command-internal/target/resolve-project-target.ts b/apps/cli/src/command-internal/target/resolve-project-target.ts new file mode 100644 index 0000000000..ee103cf65d --- /dev/null +++ b/apps/cli/src/command-internal/target/resolve-project-target.ts @@ -0,0 +1,102 @@ +import { Effect, Option } from "effect"; + +import { resolveParentScopedProjectRef } from "./parent-scoped-project-ref.ts"; +import { rejectRefShapedRemote, resolveHostedRemote, selectTarget } from "./remote-resolution.ts"; +import type { ProjectTargetFlagValues } from "./target-flags.ts"; +import { TargetNeedsProjectError } from "./target.errors.ts"; + +interface ParentProjectTargetOptions { + /** For a parent-project command such as `branches *`: `--remote` takes a ref, not a branch. */ + readonly parent: true; +} + +interface DetailedProjectTargetOptions { + /** Returns the resolved linked target, with the branch it was found through, not just its ref. */ + readonly detail: true; +} + +const selectProject = Effect.fnUntraced(function* ( + flags: ProjectTargetFlagValues, + options: { readonly valueHint?: string } = {}, +) { + return yield* selectTarget( + { + remote: flags.remote, + projectRef: flags.projectRef, + local: false, + linked: false, + }, + options, + ); +}); + +const unreachableSelection = (tag: "Local" | "DbUrl") => + Effect.die(new Error(`a project-only selector cannot select "${tag}"`)); + +const resolveHosted = Effect.fn("Target.resolveProject")(function* ( + flags: ProjectTargetFlagValues, +) { + const { selection } = yield* selectProject(flags); + switch (selection._tag) { + case "Local": + case "DbUrl": + return yield* unreachableSelection(selection._tag); + case "Default": + return yield* resolveHostedRemote({ _tag: "Linked" }); + case "Linked": + case "Ref": + case "Branch": + return yield* resolveHostedRemote(selection); + } +}); + +const resolveParent = Effect.fn("Target.resolveProject")(function* ( + flags: ProjectTargetFlagValues, +) { + const { selection, commandName } = yield* selectProject(flags, { valueHint: "" }); + switch (selection._tag) { + case "Branch": + return yield* new TargetNeedsProjectError({ + message: `${commandName} acts on a parent project, so --remote takes its project ref, not a branch name. Use --remote=.`, + }); + case "Ref": + return yield* resolveParentScopedProjectRef(Option.some(selection.ref)); + case "Linked": + case "Default": + // A branch-name word has one reading here, since `--remote=` is an error; + // only a ref-shaped word reads both ways. + yield* rejectRefShapedRemote(); + return yield* resolveParentScopedProjectRef(Option.none()); + case "Local": + case "DbUrl": + return yield* unreachableSelection(selection._tag); + } +}); + +/** + * Resolves the project a hosted-only command acts on: `--remote=` names one, and a + * bare `--remote` or no selector falls through to the project-ref chain (`SUPABASE_PROJECT_ID`, + * the link file, the picker). + */ +export function resolveProjectTarget( + flags: ProjectTargetFlagValues, +): Effect.Effect< + string, + Effect.Error>, + Effect.Services> +>; +export function resolveProjectTarget( + flags: ProjectTargetFlagValues, + options: ParentProjectTargetOptions, +): ReturnType; +export function resolveProjectTarget( + flags: ProjectTargetFlagValues, + options: DetailedProjectTargetOptions, +): ReturnType; +export function resolveProjectTarget( + flags: ProjectTargetFlagValues, + options?: ParentProjectTargetOptions | DetailedProjectTargetOptions, +) { + if (options === undefined) return Effect.map(resolveHosted(flags), (target) => target.ref); + return "parent" in options ? resolveParent(flags) : resolveHosted(flags); +} diff --git a/apps/cli/src/command-internal/target/resolve-target.integration.test.ts b/apps/cli/src/command-internal/target/resolve-target.integration.test.ts new file mode 100644 index 0000000000..0406d717f4 --- /dev/null +++ b/apps/cli/src/command-internal/target/resolve-target.integration.test.ts @@ -0,0 +1,1160 @@ +import { BunServices } from "@effect/platform-bun"; +import { describe, expect, it } from "@effect/vitest"; +import { + Cause, + Effect, + Exit, + FileSystem, + Layer, + Option, + Path, + Redacted, + Schema, + Tracer, +} from "effect"; + +import { + VALID_REF, + jsonResponse, + mockCommandPlatformApi, + mockCommandSettings, + transportFailure, +} from "../../../tests/helpers/command-mocks.ts"; +import { stripAnsi } from "../../../tests/helpers/ansi.ts"; +import { mockOutput } from "../../../tests/helpers/mocks.ts"; +import { branchRow } from "../../../tests/helpers/remote-selection.ts"; +import { AccessTokenRequiredError, InvalidAccessTokenError } from "../../auth/errors.ts"; +import { CommandPlatformApiFactory } from "../../auth/command-platform-api-factory.service.ts"; +import { rootCommandForFeatures } from "../../cli/root.ts"; +import { ProjectRefNotLinkedError } from "../../config/project-ref.errors.ts"; +import { ProjectRefResolver } from "../../config/project-ref.service.ts"; +import type { ProjectRefSource } from "../../config/project-ref.service.ts"; +import { CliArgs } from "../../shared/cli/cli-args.service.ts"; +import { prepareCliArgs } from "../../shared/cli/run.ts"; +import { commandRuntimeLayer } from "../../shared/runtime/command-runtime.layer.ts"; +import { commandTargetLayer } from "../../shared/output/command-target.layer.ts"; +import { announceTarget } from "../../shared/output/command-target.service.ts"; +import type { OutputFormat } from "../../shared/output/types.ts"; +import { + actionability, + ErrorActionabilityId, + statusCodeActionability, +} from "../../shared/telemetry/error-actionability.ts"; +import type { RemoteValue } from "./remote-value.ts"; +import { resolveTarget } from "./resolve-target.ts"; +import { renderLocalTarget } from "./target-identity.ts"; +import { TargetPolicy } from "./target-policy.ts"; +import type { TargetGroup } from "./target-policy.ts"; +import type { TargetFlagValues } from "./target-flags.ts"; + +const stringifyJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); + +const none: TargetFlagValues = { + remote: Option.none(), + local: false, + linked: false, + dbUrl: Option.none(), + projectRef: Option.none(), +}; + +const remote = (value: RemoteValue): Partial => ({ + remote: Option.some(value), +}); + +const FILE_REF = "filefilefilefilefile"; +const FLAG_REF = "flagflagflagflagflag"; +const BRANCH_REF = "branchprojectrefabcd"; +const STALE_REF = "stalestalestalestale"; +const GONE_REF = "gonegonegonegonegone"; +const SECRET_URL = + "postgresql://postgres:hunter2@db.abcdefghijklmnopqrst.supabase.co:5432/postgres"; + +const fileBranch = (name: string, overrides: Parameters[1] = {}) => + branchRow(name, { parent_project_ref: FILE_REF, ...overrides }); + +const failingListings = { + unavailable: { status: 500, message: "boom" }, + unauthorized: { status: 401, message: "Unauthorized" }, + forbidden: { status: 403, message: "Forbidden" }, + missing: { status: 404, message: "Not found" }, +} as const; + +type Listing = ReadonlyArray> | keyof typeof failingListings; + +const root = rootCommandForFeatures(); + +const projectRefResolver = (options: { readonly linked: boolean }) => { + const flags: Array> = []; + const layer = Layer.succeed(ProjectRefResolver, { + resolveRef: (flagValue) => + options.linked + ? Effect.sync(() => { + flags.push(flagValue); + const source: ProjectRefSource = Option.isSome(flagValue) ? "flag" : "file"; + return { + ref: Option.getOrElse(flagValue, () => FILE_REF), + source, + matchesLinkedRef: !Option.isSome(flagValue), + }; + }) + : Effect.fail(new ProjectRefNotLinkedError({ message: "Cannot find project ref." })), + resolve: () => Effect.die("resolve is not used by resolveTarget"), + resolveForLink: () => Effect.die("resolveForLink is not used by resolveTarget"), + resolveOptional: () => Effect.die("resolveOptional is not used by resolveTarget"), + promptProjectRef: () => Effect.die("promptProjectRef is not used by resolveTarget"), + }); + return { layer, flags }; +}; + +const run = ( + group: TargetGroup, + input: TargetFlagValues, + options: { + readonly linked?: boolean; + readonly projectId?: Option.Option; + readonly savedRef?: string; + readonly cache?: { readonly ref: string; readonly name?: string }; + readonly args?: ReadonlyArray; + readonly command?: ReadonlyArray; + readonly format?: OutputFormat; + readonly tokenFailure?: AccessTokenRequiredError | InvalidAccessTokenError; + readonly storedToken?: boolean; + readonly unreachable?: string; + readonly branches?: Listing; + readonly listings?: Readonly>; + } = {}, +) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = yield* fs.makeTempDirectoryScoped({ prefix: "resolve-target-" }); + yield* fs.makeDirectory(path.join(dir, "supabase", ".temp"), { recursive: true }); + if (options.savedRef !== undefined) { + yield* fs.writeFileString( + path.join(dir, "supabase", ".temp", "project-ref"), + options.savedRef, + ); + } + if (options.cache !== undefined) { + yield* fs.writeFileString( + path.join(dir, "supabase", ".temp", "linked-project.json"), + stringifyJson(options.cache), + ); + } + const resolver = projectRefResolver({ linked: options.linked ?? true }); + const out = mockOutput({ format: options.format ?? "text" }); + const api = mockCommandPlatformApi({ + handler: (request) => { + if (options.unreachable !== undefined) { + return Effect.fail(transportFailure(request, options.unreachable)); + } + const parentRef = new URL(request.url).pathname.split("/")[3] ?? ""; + const branches = options.listings?.[parentRef] ?? options.branches ?? []; + if (typeof branches === "string") { + const { status, message } = failingListings[branches]; + return Effect.succeed(jsonResponse(request, status, { message })); + } + return Effect.succeed(jsonResponse(request, 200, branches)); + }, + }); + const spans: Array = []; + const tracer = Tracer.make({ + span(spanOptions) { + const span = new Tracer.NativeSpan(spanOptions); + spans.push(span); + return span; + }, + }); + const exit = yield* resolveTarget(input).pipe( + Effect.tap(() => announceTarget(renderLocalTarget("127.0.0.1", 54322))), + Effect.provide( + Layer.mergeAll( + Layer.succeed(TargetPolicy, { group, consent: "none" }), + commandTargetLayer.pipe(Layer.provide(out.layer)), + out.layer, + resolver.layer, + options.tokenFailure === undefined + ? api.layer + : Layer.succeed(CommandPlatformApiFactory, { make: Effect.fail(options.tokenFailure) }), + mockCommandSettings({ + workdir: dir, + projectId: options.projectId ?? Option.none(), + ...(options.storedToken === true ? { accessToken: Option.none() } : {}), + }), + options.args === undefined + ? Layer.empty + : Layer.succeed(CliArgs, prepareCliArgs(root, options.args)), + options.command === undefined ? Layer.empty : commandRuntimeLayer(options.command), + ), + ), + Effect.withTracer(tracer), + Effect.withTracerEnabled(true), + Effect.exit, + ); + const attributes = spans.find((span) => span.name === "Target.resolve")?.attributes; + return { + exit, + stdout: out.stdoutText, + stderr: out.stderrText, + warnings: stripAnsi(out.stderrText) + .split("\n") + .filter((line) => line.startsWith("Warning: ")) + .map((line) => line.slice("Warning: ".length)), + refFlags: resolver.flags, + apiRequests: api.requests.map( + (request) => `${request.method} ${new URL(request.url).pathname}`, + ), + kind: attributes?.get("target.kind"), + selection: attributes?.get("target.selection"), + }; + }).pipe(Effect.scoped, Effect.provide(BunServices.layer)); + +const succeeded = (exit: Exit.Exit): A => { + if (!Exit.isSuccess(exit)) throw new Error(`expected success, got ${stringifyJson(exit)}`); + return exit.value; +}; + +const failureOf = (exit: Exit.Exit) => + Exit.isFailure(exit) ? Cause.squash(exit.cause) : undefined; + +describe("resolveTarget", () => { + it.effect("is local when nothing selects a target", () => + Effect.gen(function* () { + const result = yield* run("selectable", none); + expect(succeeded(result.exit)).toEqual({ kind: "local" }); + expect(result.refFlags).toEqual([]); + expect(result.warnings).toEqual([]); + expect([result.kind, result.selection]).toEqual(["local", "default"]); + }), + ); + + it.effect("stays local when only SUPABASE_PROJECT_ID is set", () => + Effect.gen(function* () { + const result = yield* run("selectable", none, { projectId: Option.some(VALID_REF) }); + expect(succeeded(result.exit)).toEqual({ kind: "local" }); + expect(result.refFlags).toEqual([]); + }), + ); + + describe("--remote", () => { + it.effect("targets the linked project when bare", () => + Effect.gen(function* () { + const result = yield* run("selectable", { ...none, ...remote({ _tag: "Linked" }) }); + expect(succeeded(result.exit)).toEqual({ + kind: "linked", + ref: FILE_REF, + source: "file", + matchesLinkedRef: true, + }); + expect(result.refFlags).toEqual([Option.none()]); + expect(result.warnings).toEqual([]); + expect([result.kind, result.selection]).toEqual(["linked", "flag"]); + }), + ); + + it.effect("passes a project ref to the ref chain and marks it a flag source", () => + Effect.gen(function* () { + const result = yield* run("selectable", { + ...none, + ...remote({ _tag: "Ref", ref: FLAG_REF }), + }); + expect(succeeded(result.exit)).toEqual({ + kind: "linked", + ref: FLAG_REF, + source: "flag", + matchesLinkedRef: false, + }); + expect(result.refFlags).toEqual([Option.some(FLAG_REF)]); + expect(result.apiRequests).toEqual([]); + }), + ); + + it.effect("keeps a database URL redacted and never resolves a project", () => + Effect.gen(function* () { + const result = yield* run("selectable", { + ...none, + ...remote({ _tag: "DbUrl", url: SECRET_URL }), + }); + const target = succeeded(result.exit); + expect(target.kind).toBe("db-url"); + if (target.kind === "db-url") expect(Redacted.value(target.url)).toBe(SECRET_URL); + expect(stringifyJson(target)).not.toContain("hunter2"); + expect(result.refFlags).toEqual([]); + expect(result.apiRequests).toEqual([]); + }), + ); + + it.effect("resolves a branch name under the linked project and announces it", () => + Effect.gen(function* () { + const result = yield* run( + "selectable", + { ...none, ...remote({ _tag: "Branch", name: "feature-x" }) }, + { + savedRef: FILE_REF, + branches: [fileBranch("feature-x"), fileBranch("other", { project_ref: FLAG_REF })], + }, + ); + expect(succeeded(result.exit)).toEqual({ + kind: "linked", + ref: BRANCH_REF, + source: "flag", + matchesLinkedRef: false, + branch: "feature-x", + parentRef: FILE_REF, + isDefault: false, + }); + expect(result.apiRequests).toEqual([`GET /v1/projects/${FILE_REF}/branches`]); + expect(result.refFlags).toEqual([]); + expect(result.stderr).toContain(`project ${FILE_REF} · branch feature-x (${BRANCH_REF})`); + }), + ); + + it.effect("names the cached parent project in the branch banner", () => + Effect.gen(function* () { + const result = yield* run( + "selectable", + { ...none, ...remote({ _tag: "Branch", name: "feature-x" }) }, + { + savedRef: FILE_REF, + cache: { ref: FILE_REF, name: "alpha" }, + branches: [fileBranch("feature-x")], + }, + ); + expect(result.stderr).toContain( + `Target: project alpha (${FILE_REF}) · branch feature-x (${BRANCH_REF})\n`, + ); + }), + ); + + it.effect("looks a branch up under the cached parent that lists the linked project", () => + Effect.gen(function* () { + const result = yield* run( + "selectable", + { ...none, ...remote({ _tag: "Branch", name: "feature-x" }) }, + { + savedRef: BRANCH_REF, + cache: { ref: FILE_REF }, + listings: { + [FILE_REF]: [fileBranch("feature-x"), fileBranch("other", { project_ref: FLAG_REF })], + }, + }, + ); + expect(succeeded(result.exit)).toMatchObject({ ref: BRANCH_REF, parentRef: FILE_REF }); + expect(result.apiRequests).toEqual([`GET /v1/projects/${FILE_REF}/branches`]); + }), + ); + + it.effect("ignores a cached parent that does not list the linked project", () => + Effect.gen(function* () { + const result = yield* run( + "selectable", + { ...none, ...remote({ _tag: "Branch", name: "feature-x" }) }, + { + savedRef: FILE_REF, + cache: { ref: STALE_REF }, + listings: { + [STALE_REF]: [fileBranch("feature-x", { project_ref: FLAG_REF })], + [FILE_REF]: [fileBranch("feature-x")], + }, + }, + ); + expect(succeeded(result.exit)).toMatchObject({ + ref: BRANCH_REF, + branch: "feature-x", + parentRef: FILE_REF, + }); + }), + ); + + it.effect("keeps the cached parent when the linked project cannot list branches", () => + Effect.gen(function* () { + const result = yield* run( + "selectable", + { ...none, ...remote({ _tag: "Branch", name: "feature-x" }) }, + { + savedRef: GONE_REF, + cache: { ref: FILE_REF }, + listings: { [FILE_REF]: [fileBranch("feature-x")], [GONE_REF]: "missing" }, + }, + ); + expect(succeeded(result.exit)).toMatchObject({ ref: BRANCH_REF, parentRef: FILE_REF }); + }), + ); + + it.effect("announces the default branch as the production project", () => + Effect.gen(function* () { + const result = yield* run( + "selectable", + { ...none, ...remote({ _tag: "Branch", name: "main" }) }, + { + savedRef: FILE_REF, + cache: { ref: FILE_REF, name: "alpha" }, + branches: [fileBranch("main", { project_ref: FILE_REF, is_default: true })], + }, + ); + expect(succeeded(result.exit)).toMatchObject({ kind: "linked", ref: FILE_REF }); + expect(result.stderr).toContain(`Target: project alpha (${FILE_REF}) · production\n`); + expect(result.stderr).not.toContain("branch main"); + }), + ); + + it.effect("announces a branch whose project is the parent as production", () => + Effect.gen(function* () { + const result = yield* run( + "selectable", + { ...none, ...remote({ _tag: "Branch", name: "main" }) }, + { savedRef: FILE_REF, branches: [fileBranch("main", { project_ref: FILE_REF })] }, + ); + expect(result.stderr).toContain(`Target: project ${FILE_REF} · production\n`); + }), + ); + + it.effect("fails with the available branches when none has that name", () => + Effect.gen(function* () { + const result = yield* run( + "selectable", + { ...none, ...remote({ _tag: "Branch", name: "other-name" }) }, + { savedRef: FILE_REF, branches: [fileBranch("feature-x")] }, + ); + expect(failureOf(result.exit)).toMatchObject({ + _tag: "LinkBranchNotFoundError", + message: `Branch "other-name" not found for project ${FILE_REF}. Available branches: feature-x.`, + }); + }), + ); + + it.effect("suggests the closest branch when the name is a near miss", () => + Effect.gen(function* () { + const result = yield* run( + "selectable", + { ...none, ...remote({ _tag: "Branch", name: "feature-y" }) }, + { savedRef: FILE_REF, branches: [fileBranch("feature-x"), fileBranch("main")] }, + ); + expect(failureOf(result.exit)).toMatchObject({ + _tag: "LinkBranchNotFoundError", + message: `Branch "feature-y" not found for project ${FILE_REF}. Available branches: feature-x, main. Did you mean "feature-x"?`, + }); + }), + ); + + it.effect("suggests the branch when two letters of the name are swapped", () => + Effect.gen(function* () { + const result = yield* run( + "selectable", + { ...none, ...remote({ _tag: "Branch", name: "mian" }) }, + { + savedRef: FILE_REF, + branches: [fileBranch("main", { project_ref: FILE_REF, is_default: true })], + }, + ); + expect(failureOf(result.exit)).toMatchObject({ _tag: "LinkBranchNotFoundError" }); + expect(String((failureOf(result.exit) as Error).message)).toContain( + 'Available branches: main. Did you mean "main"?', + ); + }), + ); + + it.effect("suggests the lowercased ref when a 20-letter value has uppercase letters", () => + Effect.gen(function* () { + const result = yield* run( + "selectable", + { ...none, ...remote({ _tag: "Branch", name: "ABCDEFGHIJKLMNOPQRST" }) }, + { savedRef: FILE_REF, branches: [fileBranch("main")] }, + ); + expect(failureOf(result.exit)).toMatchObject({ + _tag: "LinkBranchNotFoundError", + message: `Branch "ABCDEFGHIJKLMNOPQRST" not found for project ${FILE_REF}. Available branches: main.\n If you meant a project ref: refs are exactly 20 lowercase letters (try "abcdefghijklmnopqrst").`, + }); + }), + ); + + it.effect("gives no ref hint to a mixed-case value that is not 20 letters", () => + Effect.gen(function* () { + const result = yield* run( + "selectable", + { ...none, ...remote({ _tag: "Branch", name: "ABCDEFGHIJKLMNOPQRS" }) }, + { savedRef: FILE_REF, branches: [fileBranch("main")] }, + ); + expect(String((failureOf(result.exit) as Error).message)).not.toContain( + "If you meant a project ref", + ); + }), + ); + + it.effect("tells an expired token to log in instead of showing the response body", () => + Effect.gen(function* () { + const result = yield* run( + "selectable", + { ...none, ...remote({ _tag: "Branch", name: "feature-x" }) }, + { savedRef: FILE_REF, branches: "unauthorized" }, + ); + const error = failureOf(result.exit); + expect(error).toMatchObject({ _tag: "LinkBranchListStatusError", status: 401 }); + expect(String((error as Error).message)).toBe( + "Authentication failed: your access token is invalid or has expired. Run `supabase login` to re-authenticate.", + ); + }), + ); + + it.effect("asks to link the parent project when no project is linked", () => + Effect.gen(function* () { + const result = yield* run("selectable", { + ...none, + ...remote({ _tag: "Branch", name: "feature-x" }), + }); + expect(failureOf(result.exit)).toMatchObject({ _tag: "LinkBranchNotLinkedError" }); + expect(result.apiRequests).toEqual([]); + }), + ); + + it.effect("suggests the lowercased ref when no project is linked", () => + Effect.gen(function* () { + const result = yield* run("selectable", { + ...none, + ...remote({ _tag: "Branch", name: "ABCDEFGHIJKLMNOPQRST" }), + }); + expect(failureOf(result.exit)).toMatchObject({ + _tag: "LinkBranchNotLinkedError", + message: expect.stringContaining( + 'If you meant a project ref: refs are exactly 20 lowercase letters (try "abcdefghijklmnopqrst").', + ), + }); + }), + ); + + it.effect("fails with not-linked instead of falling back to local when it cannot resolve", () => + Effect.gen(function* () { + const result = yield* run( + "selectable", + { ...none, ...remote({ _tag: "Linked" }) }, + { linked: false }, + ); + expect(failureOf(result.exit)).toMatchObject({ _tag: "ProjectRefNotLinkedError" }); + }), + ); + }); + + describe("a bare --remote followed by a word", () => { + const args = ["db", "pull", "--remote", "feature-x"]; + const command = ["db", "pull"]; + const bare = { ...none, ...remote({ _tag: "Linked" }) }; + const options = `For the branch: supabase db pull --remote=feature-x. For the linked project: supabase db pull feature-x --remote`; + const placeholderOptions = `For the branch: supabase db pull --remote=. For the linked project, put --remote last: supabase db pull --remote`; + + it.effect("fails when the word is a branch of the linked project", () => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + args, + command, + savedRef: FILE_REF, + branches: [fileBranch("feature-x")], + }); + expect(failureOf(result.exit)).toMatchObject({ + _tag: "TargetRemoteAmbiguousError", + message: `"feature-x" is a branch of project ${FILE_REF}. ${options}`, + }); + expect(result.refFlags).toEqual([]); + }), + ); + + it.effect("checks the branches of the linked project when the cached parent is stale", () => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + args, + command, + savedRef: FILE_REF, + cache: { ref: STALE_REF }, + listings: { + [STALE_REF]: [fileBranch("other", { project_ref: FLAG_REF })], + [FILE_REF]: [fileBranch("feature-x")], + }, + }); + expect(failureOf(result.exit)).toMatchObject({ + _tag: "TargetRemoteAmbiguousError", + message: `"feature-x" is a branch of project ${FILE_REF}. ${options}`, + }); + }), + ); + + it.effect("resolves the linked project when the word names no branch", () => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + args, + savedRef: FILE_REF, + branches: [fileBranch("other")], + }); + expect(succeeded(result.exit)).toMatchObject({ kind: "linked", ref: FILE_REF }); + }), + ); + + it.effect.each([ + { name: "a project is linked", options: { savedRef: FILE_REF } }, + { name: "no project is linked", options: { linked: false } }, + ])( + "fails before any lookup when the word is shaped like a project ref and $name", + ({ options: linkState }) => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + ...linkState, + args: ["db", "pull", "--remote", FLAG_REF], + command, + branches: [fileBranch("feature-x")], + }); + expect(failureOf(result.exit)).toMatchObject({ + _tag: "TargetRemoteAmbiguousError", + message: `"${FLAG_REF}" after --remote looks like a project ref. For that project: supabase db pull --remote=${FLAG_REF}. To keep it as an operand, put --remote last: supabase db pull --remote`, + }); + expect(result.apiRequests).toEqual([]); + expect(result.refFlags).toEqual([]); + }), + ); + + it.effect("names the branch of a linked, named project", () => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + args, + command, + savedRef: FILE_REF, + cache: { ref: FILE_REF, name: "alpha" }, + branches: [fileBranch("feature-x")], + }); + expect(failureOf(result.exit)).toMatchObject({ + message: `"feature-x" is a branch of alpha (${FILE_REF}). ${options}`, + }); + }), + ); + + it.effect("fails when the branches cannot be listed to tell, keeping the cause", () => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + args, + command, + savedRef: FILE_REF, + branches: "unavailable", + }); + const error = failureOf(result.exit); + expect(error).toMatchObject({ + _tag: "TargetRemoteAmbiguousError", + message: `Can't tell whether the word after --remote is a branch (listing the branches of project ${FILE_REF} failed: HTTP 500). ${placeholderOptions}`, + status: 500, + cause: { _tag: "LinkBranchListStatusError", status: 500 }, + }); + expect(error).not.toHaveProperty("suggestion"); + }), + ); + + it.effect("names the transport failure when the listing never gets a response", () => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + args, + command, + savedRef: FILE_REF, + unreachable: "ECONNREFUSED", + }); + const error = failureOf(result.exit); + expect(error).toMatchObject({ + _tag: "TargetRemoteAmbiguousError", + message: `Can't tell whether the word after --remote is a branch (listing the branches of project ${FILE_REF} failed: ECONNREFUSED). ${placeholderOptions}`, + cause: { _tag: "LinkBranchListNetworkError" }, + }); + expect(error).not.toHaveProperty("status"); + expect(error).not.toHaveProperty("suggestion"); + }), + ); + + it.effect("keeps URL credentials out of a transport failure it names", () => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + args, + command, + savedRef: FILE_REF, + unreachable: + "connect ECONNREFUSED https://proxy-user:hunter2@proxy.test:8080\nsecond line", + }); + const message = String((failureOf(result.exit) as Error).message); + expect(message).toContain( + `failed: connect ECONNREFUSED https://proxy.test:8080). ${placeholderOptions}`, + ); + expect(message).not.toContain("hunter2"); + expect(message).not.toContain("second line"); + }), + ); + + it.effect.each([ + { + name: "unauthorized with a token from SUPABASE_ACCESS_TOKEN", + branches: "unauthorized", + storedToken: false, + status: 401, + suggestion: "Set SUPABASE_ACCESS_TOKEN to a valid access token.", + actionability: actionability.authToken, + }, + { + name: "unauthorized with a stored token", + branches: "unauthorized", + storedToken: true, + status: 401, + suggestion: "Run `supabase login` with an account that can list this project's branches.", + actionability: statusCodeActionability(401), + }, + { + name: "forbidden", + branches: "forbidden", + storedToken: false, + status: 403, + suggestion: "Use an access token that can read this project's branches.", + actionability: statusCodeActionability(403), + }, + { + name: "not found", + branches: "missing", + storedToken: false, + status: 404, + suggestion: `If ${FILE_REF} is itself a preview branch, link its parent project first: supabase link --project-ref `, + actionability: actionability.provideFlags, + }, + ] as const)("suggests the fix when the branch listing is $name", (testCase) => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + args, + command, + savedRef: FILE_REF, + branches: testCase.branches, + storedToken: testCase.storedToken, + }); + const error = failureOf(result.exit); + expect(error).toMatchObject({ + _tag: "TargetRemoteAmbiguousError", + message: `Can't tell whether the word after --remote is a branch (listing the branches of project ${FILE_REF} failed: HTTP ${testCase.status}). ${placeholderOptions}`, + suggestion: testCase.suggestion, + status: testCase.status, + }); + expect((error as { [ErrorActionabilityId]: unknown })[ErrorActionabilityId]).toEqual( + testCase.actionability, + ); + }), + ); + + it.effect.each([ + { + name: "no access token", + failure: new AccessTokenRequiredError({ message: "Access token not provided." }), + reason: "no access token is set", + suggestion: "Run `supabase login` with an account that can list this project's branches.", + actionability: actionability.authLogin, + }, + { + name: "a malformed stored access token", + failure: new InvalidAccessTokenError({ message: "Invalid access token format." }), + reason: "the stored access token is not valid", + suggestion: "Run `supabase login` with an account that can list this project's branches.", + actionability: actionability.authLogin, + }, + { + name: "a malformed SUPABASE_ACCESS_TOKEN", + failure: new InvalidAccessTokenError({ + message: "Invalid access token format.", + source: "env", + }), + reason: "SUPABASE_ACCESS_TOKEN is not a valid access token", + suggestion: "Set SUPABASE_ACCESS_TOKEN to a valid access token.", + actionability: actionability.authToken, + }, + ])("names the cause and how to fix it when the listing has $name", (testCase) => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + args, + command, + savedRef: FILE_REF, + tokenFailure: testCase.failure, + }); + const error = failureOf(result.exit); + expect(error).toMatchObject({ + _tag: "TargetRemoteAmbiguousError", + message: `Can't tell whether the word after --remote is a branch (listing the branches of project ${FILE_REF} failed because ${testCase.reason}). ${placeholderOptions}`, + suggestion: testCase.suggestion, + cause: testCase.failure, + }); + expect(error).not.toHaveProperty("status"); + expect((error as { [ErrorActionabilityId]: unknown })[ErrorActionabilityId]).toEqual( + testCase.actionability, + ); + }), + ); + + it.effect.each(["text", "json"] as const)( + "never prints a NAME=value pair after --remote when the listing is forbidden (%s)", + (format) => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + args: ["secrets", "set", "--remote", "STRIPE_KEY=sk_live_secret"], + command: ["secrets", "set"], + format, + savedRef: FILE_REF, + branches: "forbidden", + }); + expect(succeeded(result.exit)).toMatchObject({ kind: "linked", ref: FILE_REF }); + expect(result.apiRequests).toEqual([]); + expect(result.stdout + result.stderr).not.toContain("sk_live_secret"); + }), + ); + + it.effect("never echoes a word it could not check against the branches", () => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + args: ["secrets", "unset", "--remote", "sk_live_secret"], + command: ["secrets", "unset"], + savedRef: FILE_REF, + branches: "forbidden", + }); + const error = failureOf(result.exit); + expect(error).toMatchObject({ + _tag: "TargetRemoteAmbiguousError", + message: `Can't tell whether the word after --remote is a branch (listing the branches of project ${FILE_REF} failed: HTTP 403). For the branch: supabase secrets unset --remote=. For the linked project, put --remote last: supabase secrets unset --remote`, + }); + expect(stringifyJson(error)).not.toContain("sk_live_secret"); + }), + ); + + it.effect("fails for a word one edit from a branch, naming the closest branch", () => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + args: ["db", "pull", "--remote", "feature-y"], + command, + savedRef: FILE_REF, + branches: [fileBranch("feature-x"), fileBranch("main")], + }); + expect(failureOf(result.exit)).toMatchObject({ + _tag: "TargetRemoteAmbiguousError", + message: `"feature-y" isn't a branch, but "feature-x" is. For the branch: supabase db pull --remote=feature-x. For the linked project: supabase db pull feature-y --remote`, + }); + expect(result.refFlags).toEqual([]); + }), + ); + + it.effect.each([ + { name: "two edits from a branch", word: "feature-yz", branches: [fileBranch("feature-x")] }, + { name: "one edit from a short branch", word: "den", branches: [fileBranch("dev")] }, + { + name: "one edit from the default branch", + word: "mail", + branches: [fileBranch("main", { project_ref: FILE_REF, is_default: true })], + }, + ])("keeps a word that is $name as an operand", ({ word, branches }) => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + args: ["db", "pull", "--remote", word], + command, + savedRef: FILE_REF, + branches, + }); + expect(succeeded(result.exit)).toMatchObject({ kind: "linked", ref: FILE_REF }); + }), + ); + + it.effect("fails for a word with two letters swapped from a branch", () => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + args: ["db", "pull", "--remote", "stagign"], + command, + savedRef: FILE_REF, + branches: [fileBranch("staging")], + }); + expect(failureOf(result.exit)).toMatchObject({ + _tag: "TargetRemoteAmbiguousError", + message: `"stagign" isn't a branch, but "staging" is. For the branch: supabase db pull --remote=staging. For the linked project: supabase db pull stagign --remote`, + }); + }), + ); + + it.effect("fails for a word that differs from the default branch only by case", () => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + args: ["db", "pull", "--remote", "Main"], + command, + savedRef: FILE_REF, + branches: [fileBranch("main", { project_ref: FILE_REF, is_default: true })], + }); + expect(failureOf(result.exit)).toMatchObject({ + message: `"Main" isn't a branch, but "main" is. For the branch: supabase db pull --remote=main. For the linked project: supabase db pull Main --remote`, + }); + }), + ); + + it.effect("fails for a word that differs from a branch only by case", () => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + args: ["db", "pull", "--remote", "Feature-X"], + command, + savedRef: FILE_REF, + branches: [fileBranch("feature-x")], + }); + expect(failureOf(result.exit)).toMatchObject({ + message: `"Feature-X" isn't a branch, but "feature-x" is. For the branch: supabase db pull --remote=feature-x. For the linked project: supabase db pull Feature-X --remote`, + }); + }), + ); + + it.effect("never echoes the userinfo of a word that carries credentials", () => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + args: ["db", "pull", "--remote", "hunter2:pw@feature-x"], + command, + savedRef: FILE_REF, + branches: [fileBranch("feature-x")], + }); + const message = String((failureOf(result.exit) as Error).message); + expect(message).toContain(`"feature-x" is a branch`); + expect(message).not.toContain("hunter2"); + }), + ); + + it.effect.each([ + { name: "a path", args: ["storage", "ls", "--remote", "./photos"] }, + { name: "a URL", args: ["storage", "ls", "--remote", "ss:///bucket"] }, + { name: "a flag", args: ["db", "push", "--remote", "--dry-run"] }, + { name: "nothing", args: ["db", "push", "--remote"] }, + ])("does not look up branches when the next token is $name", ({ args: tokens }) => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + args: tokens, + savedRef: FILE_REF, + branches: [fileBranch("feature-x")], + }); + expect(succeeded(result.exit)).toMatchObject({ kind: "linked" }); + expect(result.apiRequests).toEqual([]); + }), + ); + + it.effect.each([ + { name: "seed buckets", raw: ["seed", "--remote", "buckets"] }, + { name: "db push", raw: ["db", "--remote", "push"] }, + ])("does not look up the $name subcommand as a branch", ({ raw }) => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + args: raw, + savedRef: FILE_REF, + branches: "unavailable", + }); + expect(succeeded(result.exit)).toMatchObject({ kind: "linked", ref: FILE_REF }); + expect(result.apiRequests).toEqual([]); + }), + ); + + it.effect("still fails for a branch the command would otherwise take as its operand", () => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + args: ["db", "pull", "--remote", "feature-x"], + command, + savedRef: FILE_REF, + branches: [fileBranch("feature-x")], + }); + expect(failureOf(result.exit)).toMatchObject({ + _tag: "TargetRemoteAmbiguousError", + message: `"feature-x" is a branch of project ${FILE_REF}. ${options}`, + }); + }), + ); + + it.effect("does not look up branches when no project is linked", () => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { + args, + branches: [fileBranch("feature-x")], + }); + expect(succeeded(result.exit)).toMatchObject({ kind: "linked" }); + expect(result.apiRequests).toEqual([]); + }), + ); + }); + + describe("a repeated --remote", () => { + const command = ["db", "push"]; + const bare = { ...none, ...remote({ _tag: "Linked" }) }; + + it.effect("fails before anything runs when the values differ", () => + Effect.gen(function* () { + const result = yield* run( + "selectable", + { ...none, ...remote({ _tag: "Branch", name: "feature-x" }) }, + { + args: ["db", "push", "--remote=feature-x", "--remote"], + command, + savedRef: FILE_REF, + branches: [fileBranch("feature-x")], + }, + ); + const error = failureOf(result.exit); + expect(error).toMatchObject({ + _tag: "TargetSelectorConflictError", + flags: ["--remote"], + message: "--remote is given more than once with different values; pass it once.", + suggestion: "Pass --remote once, as --remote or --remote=.", + }); + expect((error as { [ErrorActionabilityId]: unknown })[ErrorActionabilityId]).toEqual( + actionability.provideFlags, + ); + expect(result.apiRequests).toEqual([]); + expect(result.refFlags).toEqual([]); + }), + ); + + it.effect.each([ + { name: "the same value", args: ["db", "push", "--remote=feature-x", "--remote=feature-x"] }, + { name: "a bare switch", args: ["db", "push", "--remote", "--remote"] }, + ])("accepts $name given twice", ({ args }) => + Effect.gen(function* () { + const result = yield* run("selectable", bare, { args, command, savedRef: FILE_REF }); + expect(succeeded(result.exit)).toMatchObject({ kind: "linked", ref: FILE_REF }); + }), + ); + }); + + describe("deprecated selectors", () => { + it.effect( + "warns that --db-url is deprecated and offers its project ref, without echoing secrets", + () => + Effect.gen(function* () { + const result = yield* run( + "selectable", + { ...none, dbUrl: Option.some(SECRET_URL) }, + { + args: ["db", "push", "--db-url", SECRET_URL, "--password", "hunter2"], + command: ["db", "push"], + }, + ); + expect(succeeded(result.exit)).toMatchObject({ kind: "db-url" }); + expect(result.warnings).toEqual([ + "--db-url is deprecated and will be removed in v4. Use --remote= instead, or --remote=abcdefghijklmnopqrst with an access token set", + ]); + expect(result.warnings.join("\n")).not.toContain("hunter2"); + }), + ); + + it.effect("does not warn for --remote itself", () => + Effect.gen(function* () { + const result = yield* run("selectable", { ...none, ...remote({ _tag: "Linked" }) }); + expect(result.warnings).toEqual([]); + }), + ); + }); + + it.effect("rejects conflicting selectors", () => + Effect.gen(function* () { + const result = yield* run("selectable", { + ...none, + ...remote({ _tag: "Linked" }), + linked: true, + }); + expect(failureOf(result.exit)).toMatchObject({ _tag: "TargetSelectorConflictError" }); + }), + ); + + describe("the default-local banner", () => { + it.effect("hints at --remote when the checkout is linked", () => + Effect.gen(function* () { + const result = yield* run("selectable", none, { + savedRef: FILE_REF, + cache: { ref: FILE_REF, name: "alpha" }, + }); + expect(succeeded(result.exit)).toEqual({ kind: "local" }); + expect(result.stderr).toBe( + `Target: local stack (127.0.0.1:54322) · default; pass --remote for alpha (${FILE_REF})\n`, + ); + }), + ); + + it.effect("hints with the bare ref when the cache does not name the linked project", () => + Effect.gen(function* () { + const result = yield* run("selectable", none, { savedRef: FILE_REF }); + expect(result.stderr).toBe( + `Target: local stack (127.0.0.1:54322) · default; pass --remote for ${FILE_REF}\n`, + ); + }), + ); + + it.effect("hints from SUPABASE_PROJECT_ID when nothing is linked on disk", () => + Effect.gen(function* () { + const result = yield* run("selectable", none, { projectId: Option.some(FILE_REF) }); + expect(result.stderr).toBe( + `Target: local stack (127.0.0.1:54322) · default; pass --remote for ${FILE_REF}\n`, + ); + }), + ); + + it.effect("names the project SUPABASE_PROJECT_ID points at when the cache knows it", () => + Effect.gen(function* () { + const result = yield* run("selectable", none, { + projectId: Option.some(FILE_REF), + cache: { ref: FILE_REF, name: "alpha" }, + }); + expect(result.stderr).toBe( + `Target: local stack (127.0.0.1:54322) · default; pass --remote for alpha (${FILE_REF})\n`, + ); + }), + ); + + it.effect("prefers SUPABASE_PROJECT_ID over the link file", () => + Effect.gen(function* () { + const result = yield* run("selectable", none, { + projectId: Option.some(FILE_REF), + savedRef: FLAG_REF, + }); + expect(result.stderr).toBe( + `Target: local stack (127.0.0.1:54322) · default; pass --remote for ${FILE_REF}\n`, + ); + }), + ); + + it.effect("adds no hint when SUPABASE_PROJECT_ID is not a project ref", () => + Effect.gen(function* () { + const result = yield* run("selectable", none, { projectId: Option.some("not-a-ref") }); + expect(result.stderr).toBe("Target: local stack (127.0.0.1:54322)\n"); + }), + ); + + it.effect( + "adds no hint when SUPABASE_PROJECT_ID is not a project ref, even if a link exists", + () => + Effect.gen(function* () { + const result = yield* run("selectable", none, { + projectId: Option.some("not-a-ref"), + savedRef: FILE_REF, + }); + expect(result.stderr).toBe("Target: local stack (127.0.0.1:54322)\n"); + }), + ); + + it.effect("adds no hint when the checkout is not linked", () => + Effect.gen(function* () { + const result = yield* run("selectable", none, { cache: { ref: FILE_REF, name: "alpha" } }); + expect(result.stderr).toBe("Target: local stack (127.0.0.1:54322)\n"); + }), + ); + + it.effect("warns that --local is not needed and adds no hint, even in a linked checkout", () => + Effect.gen(function* () { + const result = yield* run("selectable", { ...none, local: true }, { savedRef: FILE_REF }); + expect(succeeded(result.exit)).toEqual({ kind: "local" }); + expect(result.refFlags).toEqual([]); + expect(stripAnsi(result.stderr)).toBe( + "Warning: --local isn't needed (local is the default) and will be removed in v4\nTarget: local stack (127.0.0.1:54322)\n", + ); + }), + ); + }); + + it.effect.each(["local-only", "account", "hosted", "removed"] satisfies Array)( + "dies for a %s command", + (group) => + Effect.gen(function* () { + const result = yield* run(group, none); + expect(Exit.isFailure(result.exit)).toBe(true); + if (Exit.isFailure(result.exit)) { + expect(Cause.pretty(result.exit.cause)).toContain( + "resolveTarget needs a selectable command", + ); + } + }), + ); +}); diff --git a/apps/cli/src/command-internal/target/resolve-target.ts b/apps/cli/src/command-internal/target/resolve-target.ts new file mode 100644 index 0000000000..70c8554abb --- /dev/null +++ b/apps/cli/src/command-internal/target/resolve-target.ts @@ -0,0 +1,83 @@ +import { Effect, FileSystem, Option, Path, Redacted } from "effect"; + +import { CommandSettings } from "../../config/command-settings.service.ts"; +import { PROJECT_REF_PATTERN, type ProjectRefSource } from "../../config/project-ref.service.ts"; +import { CommandTarget } from "../../shared/output/command-target.service.ts"; +import { readLinkSnapshotSoft } from "../../shared/config/link-snapshot.ts"; +import { resolveHostedRemote, selectTarget } from "./remote-resolution.ts"; +import type { TargetFlagValues } from "./target-flags.ts"; +import { linkedAlternativeHint } from "./target-identity.ts"; +import { TargetPolicy } from "./target-policy.ts"; + +export type ResolvedTarget = + | { readonly kind: "local" } + | { + readonly kind: "linked"; + readonly ref: string; + readonly source: ProjectRefSource; + readonly matchesLinkedRef: boolean; + /** The branch name or UUID `--remote` was given, when it named one. */ + readonly branch?: string | undefined; + /** The linked project the branch was found under, when it was looked up through one. */ + readonly parentRef?: string | undefined; + /** Whether the branch is the production branch, when its lookup said so. */ + readonly isDefault?: boolean | undefined; + /** The IPv4 pooler URL the link recorded, present only when `ref` is the linked ref. */ + readonly poolerUrl?: string | undefined; + } + | { readonly kind: "db-url"; readonly url: Redacted.Redacted }; + +export type LinkedTarget = Extract; + +type SelectedBy = "flag" | "default"; + +const hintLinkedAlternative = Effect.fnUntraced(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const { workdir, projectId } = yield* CommandSettings; + const link = yield* readLinkSnapshotSoft(fs, path, workdir); + const linkedRef = Option.filter( + Option.orElse(projectId, () => link.ref), + (ref) => PROJECT_REF_PATTERN.test(ref), + ); + if (Option.isNone(linkedRef)) return; + const cell = yield* Effect.serviceOption(CommandTarget); + if (Option.isNone(cell)) return; + yield* cell.value.hintLocal(linkedAlternativeHint({ ref: linkedRef.value, cache: link.parent })); +}); + +/** + * Resolves the target a `selectable` command acts on: `--remote` (or a deprecated alias) picks a + * linked project, a branch or a database URL, and no selector means the local stack. + */ +export const resolveTarget = Effect.fn("Target.resolve")(function* (flags: TargetFlagValues) { + const policy = yield* TargetPolicy; + if (policy.group !== "selectable") { + return yield* Effect.die( + new Error(`resolveTarget needs a selectable command, not "${policy.group}"`), + ); + } + const { selection } = yield* selectTarget(flags); + + const finish = Effect.fnUntraced(function* (target: ResolvedTarget, selectedBy: SelectedBy) { + yield* Effect.annotateCurrentSpan({ + "target.kind": target.kind, + "target.selection": selectedBy, + }); + return target; + }); + + switch (selection._tag) { + case "Local": + return yield* finish({ kind: "local" }, "flag"); + case "DbUrl": + return yield* finish({ kind: "db-url", url: Redacted.make(selection.url) }, "flag"); + case "Default": + yield* hintLinkedAlternative(); + return yield* finish({ kind: "local" }, "default"); + case "Linked": + case "Ref": + case "Branch": + return yield* finish(yield* resolveHostedRemote(selection), "flag"); + } +}); diff --git a/apps/cli/src/command-internal/target/target-deprecation.ts b/apps/cli/src/command-internal/target/target-deprecation.ts new file mode 100644 index 0000000000..33b1e1447a --- /dev/null +++ b/apps/cli/src/command-internal/target/target-deprecation.ts @@ -0,0 +1,93 @@ +import { BRANCH_PROJECT_REF_PATTERN } from "../ref-patterns.ts"; +import type { RemoteValue } from "./remote-value.ts"; +import { DB_URL_PATTERN, isBooleanLiteral, stripUserinfo } from "./remote-vocabulary.ts"; +import { joinFlags, type TargetDeprecation } from "./target-selector.ts"; + +const SAFE_TOKEN = /^[\w@%+=:,./-]+$/; + +export const shellQuote = (token: string): string => + SAFE_TOKEN.test(token) ? token : `'${token.replaceAll("'", `'\\''`)}'`; + +const BRANCH_BY_REF = "--remote="; + +/** The `--remote` spelling for `value`; never echoes a database URL or a branch name's userinfo. */ +export const remoteFlagText = (value: RemoteValue): string => { + switch (value._tag) { + case "Linked": + return "--remote"; + case "Ref": + return `--remote=${value.ref}`; + case "Branch": + return isBooleanLiteral(value.name) + ? BRANCH_BY_REF + : `--remote=${shellQuote(stripUserinfo(value.name))}`; + case "DbUrl": + return "--remote="; + } +}; + +/** The `--remote` form a deprecated flag's value maps to, with a placeholder for the value. */ +const replacementOf = (value: RemoteValue): string => { + switch (value._tag) { + case "Linked": + return "--remote"; + case "Ref": + return "--remote="; + case "Branch": + return isBooleanLiteral(value.name) ? BRANCH_BY_REF : "--remote="; + case "DbUrl": + return DB_URL_PATTERN.test(value.url) ? "--remote=" : "--remote="; + } +}; + +/** The ref a Supabase connection string names: `db..` or a `postgres.` user. */ +const refFromDbUrl = (url: string, projectHost: string): string | undefined => { + try { + const parsed = new URL(url); + const host = parsed.hostname; + if (host.startsWith("db.") && host.endsWith(`.${projectHost}`)) { + const ref = host.slice("db.".length, host.length - projectHost.length - 1); + if (BRANCH_PROJECT_REF_PATTERN.test(ref)) return ref; + } + const poolerRef = /^postgres\.([a-z]{20})$/.exec(decodeURIComponent(parsed.username))?.[1]; + return poolerRef; + } catch { + return undefined; + } +}; + +/** The first sentence of every alias warning: the deprecated flags, and that v4 removes them. */ +export const deprecationLead = (flags: ReadonlyArray): string => + `${joinFlags(flags)} ${flags.length > 1 ? "are" : "is"} deprecated and will be removed in v4.`; + +export const LOCAL_NOT_NEEDED_MESSAGE = + "--local isn't needed (local is the default) and will be removed in v4"; + +const aliasMessage = ( + deprecation: Extract, + projectHost: string, +): string => { + const { flags, remote } = deprecation; + const lead = deprecationLead(flags); + + if (remote._tag === "Branch" && isBooleanLiteral(remote.name)) { + return `${lead} Reach a branch named "${remote.name}" with ${BRANCH_BY_REF} instead`; + } + if (remote._tag === "DbUrl" && !DB_URL_PATTERN.test(remote.url)) { + return `${lead} Convert the connection string to a postgresql:// URL and pass it as --remote= instead`; + } + const ref = remote._tag === "DbUrl" ? refFromDbUrl(remote.url, projectHost) : undefined; + const alternative = ref === undefined ? "" : `, or --remote=${ref} with an access token set`; + return `${lead} Use ${replacementOf(remote)} instead${alternative}`; +}; + +/** The warning per deprecated selector, naming its `--remote` replacement; no URL is echoed. */ +export const deprecationMessages = ( + deprecations: ReadonlyArray, + projectHost: string, +): ReadonlyArray => + deprecations.map((deprecation) => + deprecation.kind === "local" + ? LOCAL_NOT_NEEDED_MESSAGE + : aliasMessage(deprecation, projectHost), + ); diff --git a/apps/cli/src/command-internal/target/target-deprecation.unit.test.ts b/apps/cli/src/command-internal/target/target-deprecation.unit.test.ts new file mode 100644 index 0000000000..10f7583426 --- /dev/null +++ b/apps/cli/src/command-internal/target/target-deprecation.unit.test.ts @@ -0,0 +1,142 @@ +import { describe, expect, it } from "vitest"; + +import { deprecationMessages, remoteFlagText } from "./target-deprecation.ts"; +import type { TargetDeprecation } from "./target-selector.ts"; + +const REF = "abcdefghijklmnopqrst"; +const HOST = "supabase.co"; + +describe("remoteFlagText", () => { + it.each([ + { remote: { _tag: "Linked" }, expected: "--remote" }, + { remote: { _tag: "Ref", ref: REF }, expected: `--remote=${REF}` }, + { remote: { _tag: "Branch", name: "feature-x" }, expected: "--remote=feature-x" }, + { remote: { _tag: "Branch", name: "my branch" }, expected: "--remote='my branch'" }, + { remote: { _tag: "Branch", name: "u:pw@staging" }, expected: "--remote=staging" }, + { remote: { _tag: "Branch", name: "TRUE" }, expected: "--remote=" }, + { remote: { _tag: "DbUrl", url: "postgresql://u:pw@h/db" }, expected: "--remote=" }, + ] as const)("spells $remote as $expected", ({ remote, expected }) => { + expect(remoteFlagText(remote)).toBe(expected); + }); +}); + +describe("deprecationMessages", () => { + const messages = (deprecations: ReadonlyArray) => + deprecationMessages(deprecations, HOST); + + it.each(["--project-ref", "--project-id"] as const)( + "points a deprecated %s at --remote=", + (flag) => { + expect( + messages([{ kind: "remote-alias", flags: [flag], remote: { _tag: "Ref", ref: REF } }]), + ).toEqual([`${flag} is deprecated and will be removed in v4. Use --remote= instead`]); + }, + ); + + it("points a deprecated --linked at a bare --remote", () => { + expect( + messages([{ kind: "remote-alias", flags: ["--linked"], remote: { _tag: "Linked" } }]), + ).toEqual(["--linked is deprecated and will be removed in v4. Use --remote instead"]); + }); + + it("joins every alias the run used into one warning", () => { + expect( + messages([ + { + kind: "remote-alias", + flags: ["--linked", "--project-ref"], + remote: { _tag: "Ref", ref: REF }, + }, + ]), + ).toEqual([ + `--linked and --project-ref are deprecated and will be removed in v4. Use --remote= instead`, + ]); + }); + + it("points a deprecated --project-ref branch name at --remote=", () => { + expect( + messages([ + { + kind: "remote-alias", + flags: ["--project-ref"], + remote: { _tag: "Branch", name: "staging" }, + }, + ]), + ).toEqual([ + "--project-ref is deprecated and will be removed in v4. Use --remote= instead", + ]); + }); + + it("offers the project ref a Supabase database URL already names, without echoing the URL", () => { + const url = `postgresql://postgres:hunter2@db.${REF}.supabase.co:5432/postgres`; + const [message] = messages([ + { kind: "remote-alias", flags: ["--db-url"], remote: { _tag: "DbUrl", url } }, + ]); + expect(message).toBe( + `--db-url is deprecated and will be removed in v4. Use --remote= instead, or --remote=${REF} with an access token set`, + ); + expect(message).not.toContain("hunter2"); + }); + + it.each([ + { + name: "a pooler URL whose user names the project", + url: `postgresql://postgres.${REF}:pw@aws-0-eu-west-1.pooler.supabase.com:6543/postgres`, + alternative: `, or --remote=${REF} with an access token set`, + }, + { + name: "a database host outside the project domain", + url: "postgresql://postgres:pw@db.example.com:5432/postgres", + alternative: "", + }, + { + name: "a loopback address", + url: "postgresql://postgres:pw@127.0.0.1:54322/postgres", + alternative: "", + }, + { + name: "a URL that does not parse", + url: "postgresql://postgres:pw@host:notaport/postgres", + alternative: "", + }, + ])("reads the project ref out of $name only when it names one", ({ url, alternative }) => { + expect( + messages([{ kind: "remote-alias", flags: ["--db-url"], remote: { _tag: "DbUrl", url } }]), + ).toEqual([ + `--db-url is deprecated and will be removed in v4. Use --remote= instead${alternative}`, + ]); + }); + + it("asks for a postgresql:// URL when --db-url held a keyword connection string", () => { + const [message] = messages([ + { + kind: "remote-alias", + flags: ["--db-url"], + remote: { _tag: "DbUrl", url: "host=db.example.com user=postgres password=hunter2" }, + }, + ]); + expect(message).toBe( + "--db-url is deprecated and will be removed in v4. Convert the connection string to a postgresql:// URL and pass it as --remote= instead", + ); + expect(message).not.toContain("hunter2"); + }); + + it.each(["true", "No", "OFF", "1", "y", "N"])( + "says a branch named %s is reached by its ref or UUID", + (name) => { + expect( + messages([ + { kind: "remote-alias", flags: ["--project-ref"], remote: { _tag: "Branch", name } }, + ]), + ).toEqual([ + `--project-ref is deprecated and will be removed in v4. Reach a branch named "${name}" with --remote= instead`, + ]); + }, + ); + + it("says --local is not needed", () => { + expect(messages([{ kind: "local" }])).toEqual([ + "--local isn't needed (local is the default) and will be removed in v4", + ]); + }); +}); diff --git a/apps/cli/src/command-internal/target/target-flags.ts b/apps/cli/src/command-internal/target/target-flags.ts new file mode 100644 index 0000000000..00d318ea72 --- /dev/null +++ b/apps/cli/src/command-internal/target/target-flags.ts @@ -0,0 +1,247 @@ +import { Effect, Option, Redacted, Result } from "effect"; +import { Flag, GlobalFlag, Param, Primitive } from "effect/cli"; + +import { parseRemoteValue, type RemoteValue, type RemoteValueRejection } from "./remote-value.ts"; +import { REMOTE_FLAG_NAME } from "./remote-vocabulary.ts"; +import type { ResolvedTarget } from "./resolve-target.ts"; +import { remoteFlagText } from "./target-deprecation.ts"; + +type TargetSubject = "database" | "Storage API"; + +interface TargetFlagOptions { + readonly subject: TargetSubject; +} + +interface SelectableRemoteFlagVariant { + readonly kind: "selectable"; + readonly subject: TargetSubject; + readonly dbUrl: boolean; +} + +interface HostedRemoteFlagVariant { + readonly kind: "hosted"; + /** A command on a parent project, such as `branches *`: its value is a ref, not a branch name. */ + readonly parent: boolean; +} + +type RemoteFlagVariant = SelectableRemoteFlagVariant | HostedRemoteFlagVariant; + +const takesDbUrl = (variant: RemoteFlagVariant): boolean => + variant.kind === "selectable" && variant.dbUrl; + +/** What the variant's `--remote` takes; the help rows, flag description and errors read it. */ +const valueHintOf = (variant: RemoteFlagVariant): string => { + if (variant.kind === "hosted") return variant.parent ? "" : ""; + return variant.dbUrl ? "" : ""; +}; + +const acceptedValuesOf = (variant: RemoteFlagVariant): string => { + if (variant.kind === "hosted" && variant.parent) return "a project ref"; + const url = takesDbUrl(variant) ? "a postgres:// URL, " : ""; + return `${url}a project ref or a branch name`; +}; + +const remoteDescription = (variant: RemoteFlagVariant): string => { + const hint = valueHintOf(variant); + if (variant.kind === "hosted") { + return `Target another project: --remote=${hint}. Defaults to the linked project.`; + } + return variant.dbUrl + ? `Target a remote database: the linked project, or --remote=${hint}.` + : `Target a remote ${variant.subject}: the linked project, or --remote=${hint}.`; +}; + +const rejectionMessage = (variant: RemoteFlagVariant, rejection: RemoteValueRejection): string => { + const fallback = + variant.kind === "hosted" ? "use the linked project" : `target the local ${variant.subject}`; + switch (rejection._tag) { + case "Empty": + return `--remote= has an empty value. Pass --remote for the linked project or --remote=${valueHintOf(variant)}; leave it out to ${fallback}.`; + case "Off": + return `--remote doesn't take false. Leave it out to ${fallback}.`; + case "ForeignUrl": + return `--remote takes ${acceptedValuesOf(variant)}${ + rejection.ref === undefined + ? "." + : `; to target that project, use --remote=${rejection.ref}.` + }`; + case "KeywordDsn": + return `--remote takes ${acceptedValuesOf(variant)}${ + takesDbUrl(variant) + ? "; convert a connection string to a postgresql:// URL and pass --remote=." + : "." + }`; + } +}; + +const REMOTE_PRIMITIVE = Symbol.for("supabase/cli/RemotePrimitive"); + +/** + * Keeps the `Boolean` primitive tag so the parser accepts a bare `--remote` and never takes the + * next token as its value; only `--remote=` (or a boolean literal) reaches `parse`. A + * database URL is rejected here for a command with no database connection. + */ +const remotePrimitive = (variant: RemoteFlagVariant): Primitive.Primitive => + Object.assign(Object.create(Primitive.Boolean), { + [REMOTE_PRIMITIVE]: true, + parse: (raw: string) => + Result.match(parseRemoteValue(raw), { + onFailure: (rejection) => Effect.fail(rejectionMessage(variant, rejection)), + onSuccess: (value) => + value._tag === "DbUrl" && !takesDbUrl(variant) + ? Effect.fail(`--remote takes ${acceptedValuesOf(variant)}.`) + : Effect.succeed(value), + }), + }); + +/** + * Whether a flag was declared by {@link targetFlags}, {@link projectTargetFlags} or the `seed` + * globals as `--remote`. + */ +export const isRemoteFlag = (single: Param.Single): boolean => + REMOTE_PRIMITIVE in single.primitiveType; + +/** `--remote`: the linked project when bare, else the project, branch or database it names. */ +const remoteFlag = (variant: RemoteFlagVariant): Flag.Flag> => + Param.makeSingle({ + kind: Param.flagKind, + name: REMOTE_FLAG_NAME, + primitiveType: remotePrimitive(variant), + typeName: valueHintOf(variant), + }).pipe(Flag.withDescription(remoteDescription(variant)), Flag.optional); + +const localFlag = () => + Flag.Boolean("local").pipe( + Flag.withDescription( + "Deprecated, removed in v4: local is the default, so --local is not needed.", + ), + Flag.withDefault(false), + Flag.withHidden, + ); + +const linkedFlag = () => + Flag.Boolean("linked").pipe( + Flag.withDescription("Deprecated, removed in v4: use --remote."), + Flag.withDefault(false), + Flag.withHidden, + ); + +const dbUrlFlag = () => + Flag.String("db-url").pipe( + Flag.withDescription("Deprecated, removed in v4: use --remote=."), + Flag.optional, + Flag.withHidden, + ); + +const projectRefFlag = () => + Flag.String("project-ref").pipe( + Flag.withDescription("Deprecated, removed in v4: use --remote=."), + Flag.optional, + Flag.withHidden, + ); + +type TargetFlagDeclarations = { + readonly remote: Flag.Flag>; + readonly local: Flag.Flag; + readonly linked: Flag.Flag; + readonly projectRef: Flag.Flag>; +}; + +type DbUrlFlagDeclaration = { readonly dbUrl: Flag.Flag> }; + +/** The decoded flags every hosted command's selector is read from. */ +export interface ProjectTargetFlagValues { + readonly remote: Option.Option; + readonly projectRef: Option.Option; +} + +/** The decoded selector flags of a command that can also act on the local stack. */ +export interface TargetFlagValues extends ProjectTargetFlagValues { + readonly local: boolean; + readonly linked: boolean; + /** `gen types` only: the deprecated alias of `--project-ref`. */ + readonly projectId?: Option.Option; + /** The deprecated `--db-url`; absent on a command with no database connection. */ + readonly dbUrl?: Option.Option; +} + +/** The decoded flags of a command whose `targetFlags` declared `dbUrl: true`. */ +export interface TargetFlagValuesWithDbUrl extends TargetFlagValues { + readonly dbUrl: Option.Option; +} + +/** + * The only declaration of `--remote` and its deprecated `--local`, `--linked`, `--db-url` and + * `--project-ref` aliases for selectable commands. + */ +export function targetFlags( + options: TargetFlagOptions & { readonly dbUrl: true }, +): TargetFlagDeclarations & DbUrlFlagDeclaration; +export function targetFlags( + options: TargetFlagOptions & { readonly dbUrl: false }, +): TargetFlagDeclarations; +export function targetFlags(options: TargetFlagOptions & { readonly dbUrl: boolean }) { + const base = { + remote: remoteFlag({ kind: "selectable", subject: options.subject, dbUrl: options.dbUrl }), + local: localFlag(), + linked: linkedFlag(), + projectRef: projectRefFlag(), + }; + return options.dbUrl ? { ...base, dbUrl: dbUrlFlag() } : base; +} + +/** + * The flags of a hosted-only command: `--remote=` and the deprecated `--project-ref` + * alias; `parent` narrows the value to a ref. + */ +export const projectTargetFlags = (options: { readonly parent?: true } = {}) => ({ + remote: remoteFlag({ kind: "hosted", parent: options.parent === true }), + projectRef: projectRefFlag(), +}); + +const remoteValueOfTarget = (target: ResolvedTarget): RemoteValue | undefined => { + switch (target.kind) { + case "local": + return undefined; + case "db-url": + return { _tag: "DbUrl", url: Redacted.value(target.url) }; + case "linked": + return target.matchesLinkedRef ? { _tag: "Linked" } : { _tag: "Ref", ref: target.ref }; + } +}; + +/** The flags that point a suggested follow-up command at `target`; a database URL is not echoed. */ +export const flagsForTarget = (target: ResolvedTarget): string => { + const value = remoteValueOfTarget(target); + return value === undefined ? "" : remoteFlagText(value); +}; + +/** + * `seed` accepts the selectors before or after its subcommand, which needs scoped globals; they + * share their definitions with {@link targetFlags}. + */ +export const seedTargetGlobalFlags = { + remote: GlobalFlag.Setting(REMOTE_FLAG_NAME)({ + flag: remoteFlag({ kind: "selectable", subject: "Storage API", dbUrl: false }), + }), + linked: GlobalFlag.Setting("linked")({ flag: linkedFlag() }), + local: GlobalFlag.Setting("local")({ flag: localFlag() }), + projectRef: GlobalFlag.Setting("project-ref")({ flag: projectRefFlag() }), +}; + +/** The `seed` selector flags a leaf reads from the group's scoped globals. */ +export const readSeedTargetFlags = Effect.fnUntraced(function* () { + return { + remote: yield* seedTargetGlobalFlags.remote, + linked: yield* seedTargetGlobalFlags.linked, + local: yield* seedTargetGlobalFlags.local, + projectRef: yield* seedTargetGlobalFlags.projectRef, + } satisfies TargetFlagValues; +}); + +/** Hidden alias of `--remote=`, accepted only by `gen types`. */ +export const deprecatedProjectIdFlag = Flag.String("project-id").pipe( + Flag.withDescription("Deprecated, removed in v4: use --remote=."), + Flag.optional, + Flag.withHidden, +); diff --git a/apps/cli/src/command-internal/target/target-flags.unit.test.ts b/apps/cli/src/command-internal/target/target-flags.unit.test.ts new file mode 100644 index 0000000000..7a2fa878fd --- /dev/null +++ b/apps/cli/src/command-internal/target/target-flags.unit.test.ts @@ -0,0 +1,391 @@ +import { BunServices } from "@effect/platform-bun"; +import { describe, expect, it } from "@effect/vitest"; +import { Effect, Layer, Option, Redacted } from "effect"; +import { CliError, CliOutput, Command, Flag } from "effect/cli"; +import type * as CliCommand from "effect/cli/Command"; + +import { formatInvalidValueMessage } from "../../shared/cli/invalid-value-message.ts"; +import { unwrapParam } from "../param-introspection.ts"; +import { + deprecatedProjectIdFlag, + flagsForTarget, + isRemoteFlag, + projectTargetFlags, + seedTargetGlobalFlags, + targetFlags, +} from "./target-flags.ts"; + +const silentFormatter: CliOutput.Formatter = { + formatCliError: () => "", + formatError: () => "", + formatErrors: () => "", + formatHelpDoc: () => "", + formatVersion: () => "", +}; + +const runParse = ( + config: Config, + args: ReadonlyArray, + parsed: Array, +) => { + const command = Command.make("target-test", config).pipe( + Command.withHandler((flags) => Effect.sync(() => parsed.push(flags))), + ); + return Command.runWith(command, { version: "0.0.0-test" })(args).pipe( + Effect.provide(Layer.mergeAll(BunServices.layer, CliOutput.layer(silentFormatter))), + ); +}; + +const parseWith = ( + config: Config, + args: ReadonlyArray, +) => { + const parsed: Array = []; + return runParse(config, args, parsed).pipe(Effect.map(() => parsed[0])); +}; + +const rejectedMessage = ( + config: Config, + args: ReadonlyArray, +) => + runParse(config, args, []).pipe( + Effect.flip, + Effect.map((error) => { + if (!CliError.isCliError(error) || error._tag !== "ShowHelp") { + throw new Error(`expected a parse failure, got ${String(error)}`); + } + const [inner] = error.errors; + if (inner?._tag !== "InvalidValue") { + throw new Error(`expected an invalid value, got ${inner?._tag}`); + } + return formatInvalidValueMessage(inner) ?? inner.message; + }), + ); + +const describeFlag = (flag: Flag.Flag) => { + const single = unwrapParam(flag)?.single; + return { + name: single?.name, + hidden: single?.hidden, + description: Option.getOrUndefined(single?.description ?? Option.none()), + }; +}; + +const REF = "abcdefghijklmnopqrst"; +const URL = "postgresql://postgres:postgres@127.0.0.1:54322/postgres"; + +describe("targetFlags", () => { + const withDbUrl = targetFlags({ subject: "database", dbUrl: true }); + + it.effect("defaults every selector to absent", () => + Effect.gen(function* () { + expect(yield* parseWith(withDbUrl, [])).toEqual({ + remote: Option.none(), + local: false, + linked: false, + dbUrl: Option.none(), + projectRef: Option.none(), + }); + }), + ); + + it.effect.each([ + { args: ["--remote"], expected: { _tag: "Linked" } }, + { args: ["--remote", "yes"], expected: { _tag: "Linked" } }, + { args: ["--remote", "y"], expected: { _tag: "Linked" } }, + { args: ["--remote=on"], expected: { _tag: "Linked" } }, + { args: ["--remote=1"], expected: { _tag: "Linked" } }, + { args: [`--remote=${REF}`], expected: { _tag: "Ref", ref: REF } }, + { args: [`--remote=${URL}`], expected: { _tag: "DbUrl", url: URL } }, + { args: ["--remote=feature-x"], expected: { _tag: "Branch", name: "feature-x" } }, + { args: ["--remote=Feature_X"], expected: { _tag: "Branch", name: "Feature_X" } }, + ])("parses $args as $expected", ({ args, expected }) => + Effect.gen(function* () { + expect(yield* parseWith(withDbUrl, args)).toMatchObject({ remote: Option.some(expected) }); + }), + ); + + it.effect("keeps the deprecated aliases parseable", () => + Effect.gen(function* () { + expect(yield* parseWith(withDbUrl, ["--local"])).toMatchObject({ local: true }); + expect(yield* parseWith(withDbUrl, ["--linked"])).toMatchObject({ linked: true }); + expect( + yield* parseWith(withDbUrl, ["--db-url", "postgresql://h/db", "--project-ref", "abc"]), + ).toMatchObject({ + dbUrl: Option.some("postgresql://h/db"), + projectRef: Option.some("abc"), + }); + expect(yield* parseWith(withDbUrl, ["--linked=false"])).toMatchObject({ linked: false }); + }), + ); + + it.effect.each([ + { args: ["--remote=false"] }, + { args: ["--remote=0"] }, + { args: ["--remote", "off"] }, + { args: ["--no-remote"] }, + ])("rejects $args by pointing at the local default", ({ args }) => + Effect.gen(function* () { + expect(yield* rejectedMessage(withDbUrl, args)).toBe( + "--remote doesn't take false. Leave it out to target the local database.", + ); + }), + ); + + it.effect.each([{ args: ["--remote", "n"] }, { args: ["--remote=n"] }])( + "rejects $args as the off switch, not a branch named n", + ({ args }) => + Effect.gen(function* () { + expect(yield* rejectedMessage(withDbUrl, args)).toBe( + "--remote doesn't take false. Leave it out to target the local database.", + ); + }), + ); + + it.effect("rejects an empty value and offers every form the command takes", () => + Effect.gen(function* () { + expect(yield* rejectedMessage(withDbUrl, ["--remote="])).toBe( + "--remote= has an empty value. Pass --remote for the linked project or --remote=; leave it out to target the local database.", + ); + }), + ); + + it.effect("names the Storage API in its local-default messages", () => + Effect.gen(function* () { + const storage = targetFlags({ subject: "Storage API", dbUrl: false }); + expect(yield* rejectedMessage(storage, ["--no-remote"])).toBe( + "--remote doesn't take false. Leave it out to target the local Storage API.", + ); + expect(yield* rejectedMessage(storage, ["--remote="])).toBe( + "--remote= has an empty value. Pass --remote for the linked project or --remote=; leave it out to target the local Storage API.", + ); + }), + ); + + it.effect.each([ + { + value: `https://${REF}.supabase.co`, + message: `--remote takes a postgres:// URL, a project ref or a branch name; to target that project, use --remote=${REF}.`, + }, + { + value: "https://example.com/db", + message: "--remote takes a postgres:// URL, a project ref or a branch name.", + }, + ])( + "rejects $value because a URL that is not postgres:// never names a branch", + ({ value, message }) => + Effect.gen(function* () { + expect(yield* rejectedMessage(withDbUrl, [`--remote=${value}`])).toBe(message); + }), + ); + + it.effect("rejects a keyword connection string without echoing it", () => + Effect.gen(function* () { + const message = yield* rejectedMessage(withDbUrl, [ + "--remote=host=db.example.com user=postgres password=hunter2", + ]); + expect(message).toBe( + "--remote takes a postgres:// URL, a project ref or a branch name; convert a connection string to a postgresql:// URL and pass --remote=.", + ); + const storage = targetFlags({ subject: "Storage API", dbUrl: false }); + expect(yield* rejectedMessage(storage, ["--remote=host=db.example.com"])).toBe( + "--remote takes a project ref or a branch name.", + ); + }), + ); + + it.effect( + "leaves the postgres:// URL out of the foreign-URL message where none is accepted", + () => + Effect.gen(function* () { + const storage = targetFlags({ subject: "Storage API", dbUrl: false }); + expect(yield* rejectedMessage(storage, ["--remote=https://example.com"])).toBe( + "--remote takes a project ref or a branch name.", + ); + }), + ); + + it("omits --db-url for commands without a database connection", () => { + const storage = targetFlags({ subject: "Storage API", dbUrl: false }); + expect(Object.keys(storage).sort()).toEqual(["linked", "local", "projectRef", "remote"]); + expect(Object.keys(withDbUrl).sort()).toEqual([ + "dbUrl", + "linked", + "local", + "projectRef", + "remote", + ]); + }); + + it.effect.each([ + { + name: "a command with no database connection", + config: targetFlags({ subject: "Storage API", dbUrl: false }), + message: "--remote takes a project ref or a branch name.", + }, + { + name: "a hosted command", + config: projectTargetFlags(), + message: "--remote takes a project ref or a branch name.", + }, + { + name: "a parent-scoped hosted command", + config: projectTargetFlags({ parent: true }), + message: "--remote takes a project ref.", + }, + { + name: "the seed group", + config: { remote: seedTargetGlobalFlags.remote.flag }, + message: "--remote takes a project ref or a branch name.", + }, + ])("rejects a database URL on $name without echoing it", ({ config, message }) => + Effect.gen(function* () { + expect(yield* rejectedMessage(config, [`--remote=${URL}`])).toBe(message); + }), + ); + + it("shows --remote and hides every deprecated alias", () => { + expect(describeFlag(withDbUrl.remote).hidden).toBeFalsy(); + for (const alias of [ + withDbUrl.local, + withDbUrl.linked, + withDbUrl.dbUrl, + withDbUrl.projectRef, + ]) { + expect(describeFlag(alias).hidden).toBe(true); + } + }); + + it("describes --remote by what the command can target", () => { + expect(describeFlag(withDbUrl.remote).description).toBe( + "Target a remote database: the linked project, or --remote=.", + ); + expect( + describeFlag(targetFlags({ subject: "Storage API", dbUrl: false }).remote).description, + ).toBe("Target a remote Storage API: the linked project, or --remote=."); + }); +}); + +describe("projectTargetFlags", () => { + const hosted = projectTargetFlags(); + + it.effect("parses --remote and the deprecated --project-ref", () => + Effect.gen(function* () { + expect(yield* parseWith(hosted, [])).toEqual({ + remote: Option.none(), + projectRef: Option.none(), + }); + expect(yield* parseWith(hosted, [`--remote=${REF}`])).toMatchObject({ + remote: Option.some({ _tag: "Ref", ref: REF }), + }); + expect(yield* parseWith(hosted, ["--project-ref", REF])).toMatchObject({ + projectRef: Option.some(REF), + }); + }), + ); + + it.effect("rejects an off value and an empty value by pointing at the linked project", () => + Effect.gen(function* () { + expect(yield* rejectedMessage(hosted, ["--no-remote"])).toBe( + "--remote doesn't take false. Leave it out to use the linked project.", + ); + expect(yield* rejectedMessage(hosted, ["--remote="])).toBe( + "--remote= has an empty value. Pass --remote for the linked project or --remote=; leave it out to use the linked project.", + ); + }), + ); + + it.effect("takes only a project ref on a parent-scoped command", () => + Effect.gen(function* () { + const parent = projectTargetFlags({ parent: true }); + expect(yield* rejectedMessage(parent, ["--remote="])).toBe( + "--remote= has an empty value. Pass --remote for the linked project or --remote=; leave it out to use the linked project.", + ); + expect(yield* rejectedMessage(parent, ["--remote=https://example.com"])).toBe( + "--remote takes a project ref.", + ); + expect(describeFlag(parent.remote).description).toBe( + "Target another project: --remote=. Defaults to the linked project.", + ); + }), + ); + + it("describes --remote as another project and hides --project-ref", () => { + expect(describeFlag(hosted.remote).description).toBe( + "Target another project: --remote=. Defaults to the linked project.", + ); + expect(describeFlag(hosted.projectRef).hidden).toBe(true); + }); +}); + +describe("deprecatedProjectIdFlag", () => { + it("is hidden from help", () => { + expect(unwrapParam(deprecatedProjectIdFlag)?.single.hidden).toBe(true); + }); + + it.effect("parses an optional value", () => + Effect.gen(function* () { + expect(yield* parseWith({ projectId: deprecatedProjectIdFlag }, [])).toEqual({ + projectId: Option.none(), + }); + expect( + yield* parseWith({ projectId: deprecatedProjectIdFlag }, ["--project-id", "abc"]), + ).toEqual({ projectId: Option.some("abc") }); + }), + ); +}); + +describe("isRemoteFlag", () => { + const singleOf = (flag: Flag.Flag) => { + const single = unwrapParam(flag)?.single; + if (single === undefined) throw new Error("expected a single flag"); + return single; + }; + + it("recognises every declaration of --remote by its primitive, not its name", () => { + expect(isRemoteFlag(singleOf(targetFlags({ subject: "database", dbUrl: true }).remote))).toBe( + true, + ); + expect(isRemoteFlag(singleOf(projectTargetFlags().remote))).toBe(true); + expect(isRemoteFlag(singleOf(seedTargetGlobalFlags.remote.flag))).toBe(true); + }); + + it("does not take a boolean flag for --remote", () => { + expect(isRemoteFlag(singleOf(targetFlags({ subject: "database", dbUrl: true }).linked))).toBe( + false, + ); + expect(isRemoteFlag(singleOf(Flag.Boolean("remote")))).toBe(false); + }); +}); + +describe("seedTargetGlobalFlags", () => { + it("takes a ref or a branch, never a database URL", () => { + expect(describeFlag(seedTargetGlobalFlags.remote.flag).description).toBe( + "Target a remote Storage API: the linked project, or --remote=.", + ); + }); + + it("shares the selector flag names with targetFlags", () => { + expect(describeFlag(seedTargetGlobalFlags.remote.flag).name).toBe("remote"); + expect(describeFlag(seedTargetGlobalFlags.linked.flag).name).toBe("linked"); + expect(describeFlag(seedTargetGlobalFlags.local.flag).name).toBe("local"); + }); +}); + +describe("flagsForTarget", () => { + it("names the flags that re-select each kind of target without echoing a db-url", () => { + expect(flagsForTarget({ kind: "local" })).toBe(""); + expect(flagsForTarget({ kind: "db-url", url: Redacted.make("postgresql://u:pw@h/db") })).toBe( + "--remote=", + ); + expect( + flagsForTarget({ kind: "linked", ref: REF, source: "file", matchesLinkedRef: true }), + ).toBe("--remote"); + }); + + it("keeps an explicit ref that is not the linked project", () => { + expect( + flagsForTarget({ kind: "linked", ref: REF, source: "flag", matchesLinkedRef: false }), + ).toBe(`--remote=${REF}`); + }); +}); diff --git a/apps/cli/src/command-internal/target/target-identity.ts b/apps/cli/src/command-internal/target/target-identity.ts new file mode 100644 index 0000000000..392b4f21cb --- /dev/null +++ b/apps/cli/src/command-internal/target/target-identity.ts @@ -0,0 +1,119 @@ +import { Option } from "effect"; + +import type { RenderedTarget } from "../../shared/output/command-target.service.ts"; +import { sanitizeInlineName } from "../http-errors.ts"; +import type { CachedLinkedProject } from "../../shared/config/link-snapshot.ts"; + +const hostWithPort = (host: string, port: number): string => + `${host.includes(":") ? `[${host}]` : host}:${port}`; + +export const renderLocalTarget = (host: string, port: number): RenderedTarget => ({ + text: `local stack (${hostWithPort(sanitizeInlineName(host), port)})`, + json: { kind: "local", host, port }, +}); + +/** Names the host and port only; the connection string's credentials never reach the banner. */ +export const renderDbUrlTarget = (host: string, port: number): RenderedTarget => ({ + text: `custom Postgres (${hostWithPort(sanitizeInlineName(host), port)})`, + json: { kind: "db-url", host, port }, +}); + +const named = (name: string | undefined, ref: string): string => { + const safeName = name === undefined ? "" : sanitizeInlineName(name); + return safeName.length === 0 ? ref : `${safeName} (${ref})`; +}; + +const productionTarget = (ref: string, name: string | undefined): RenderedTarget => ({ + text: `project ${named(name, ref)} · production`, + json: { + kind: "linked", + project_ref: ref, + ...(name === undefined ? {} : { project_name: name }), + environment: "production", + }, +}); + +/** + * A linked project's identity from the link's recorded parent alone, with no API call. Only a + * cache naming the resolved ref itself is evidence (a production project); a project that is + * merely linked, or one the cache cannot vouch for, is shown as its ref and reported `unverified`, + * because the cache can be stale or missing and never proves a branch. A ref that differs from + * the linked project's own says so. + */ +export const describeLinkedTarget = (input: { + readonly ref: string; + readonly cache: Option.Option; + /** The name the project picker showed for `ref`. */ + readonly name?: string; + /** The linked ref, when the caller compares against the linked project. */ + readonly linkedRef?: Option.Option; +}): RenderedTarget => { + const { ref } = input; + const cached = Option.getOrUndefined(input.cache); + if (cached?.ref === ref) return productionTarget(ref, cached.name); + const differsFromLinked = Option.exists( + input.linkedRef ?? Option.none(), + (linked) => linked !== ref, + ); + return { + text: `project ${named(input.name, ref)}${differsFromLinked ? " (not the linked project)" : ""}`, + json: { + kind: "linked", + project_ref: ref, + ...(input.name === undefined ? {} : { project_name: input.name }), + environment: "unverified", + }, + }; +}; + +/** The suffix of a default-local banner when linked, naming the project `--remote` would use. */ +export const linkedAlternativeHint = (input: { + readonly ref: string; + readonly cache: Option.Option; +}): string => { + const cached = Option.getOrUndefined(input.cache); + const name = cached?.ref === input.ref ? cached.name : undefined; + return `default; pass --remote for ${named(name, input.ref)}`; +}; + +/** The `config.target_is_branch` attribute: false for the default branch, omitted when unknown. */ +export const targetIsBranchAttribute = (target: { + readonly branch?: string | undefined; + readonly isDefault?: boolean | undefined; +}): { readonly "config.target_is_branch"?: boolean } => { + if (target.branch === undefined || target.isDefault === true) { + return { "config.target_is_branch": false }; + } + return target.isDefault === undefined ? {} : { "config.target_is_branch": true }; +}; + +/** + * A branch resolved to its own project ref; `branch` is absent for a UUID lookup and `parentRef` + * for anything but a name lookup. The default branch is the linked project itself, so it renders + * as production. + */ +export const renderBranchTarget = (input: { + readonly branch: Option.Option; + readonly ref: string; + readonly parentRef: Option.Option; + readonly parentName?: string | undefined; + readonly isDefault?: boolean; +}): RenderedTarget => { + if (input.isDefault === true) return productionTarget(input.ref, input.parentName); + const branch = Option.map(input.branch, sanitizeInlineName); + const parentRef = Option.getOrUndefined(input.parentRef); + const label = Option.match(branch, { + onNone: () => `branch ${input.ref}`, + onSome: (name) => `branch ${name} (${input.ref})`, + }); + return { + text: + parentRef === undefined ? label : `project ${named(input.parentName, parentRef)} · ${label}`, + json: { + kind: "linked", + project_ref: input.ref, + environment: "branch", + ...(parentRef === undefined ? {} : { parent_project_ref: parentRef }), + }, + }; +}; diff --git a/apps/cli/src/command-internal/target/target-identity.unit.test.ts b/apps/cli/src/command-internal/target/target-identity.unit.test.ts new file mode 100644 index 0000000000..ce351562f5 --- /dev/null +++ b/apps/cli/src/command-internal/target/target-identity.unit.test.ts @@ -0,0 +1,234 @@ +import { describe, expect, it } from "@effect/vitest"; +import { Option } from "effect"; + +import { + describeLinkedTarget, + linkedAlternativeHint, + renderBranchTarget, + renderDbUrlTarget, + renderLocalTarget, + targetIsBranchAttribute, +} from "./target-identity.ts"; + +const REF = "abcdefghijklmnopqrst"; +const OTHER_REF = "qrstuvwxyzabcdefghij"; + +describe("renderLocalTarget", () => { + it("renders the host and port", () => { + expect(renderLocalTarget("127.0.0.1", 54322)).toEqual({ + text: "local stack (127.0.0.1:54322)", + json: { kind: "local", host: "127.0.0.1", port: 54322 }, + }); + }); + + it("brackets an IPv6 host", () => { + expect(renderLocalTarget("::1", 54322).text).toBe("local stack ([::1]:54322)"); + }); +}); + +describe("renderDbUrlTarget", () => { + it("names the host without credentials", () => { + expect(renderDbUrlTarget("db.example.com", 5432)).toEqual({ + text: "custom Postgres (db.example.com:5432)", + json: { kind: "db-url", host: "db.example.com", port: 5432 }, + }); + }); +}); + +describe("describeLinkedTarget", () => { + it("is production when the cache names the resolved ref", () => { + const target = describeLinkedTarget({ + ref: REF, + cache: Option.some({ ref: REF, name: "alpha" }), + }); + expect(target).toEqual({ + text: `project alpha (${REF}) · production`, + json: { kind: "linked", project_ref: REF, project_name: "alpha", environment: "production" }, + }); + }); + + it("omits the name when the cache has none", () => { + const target = describeLinkedTarget({ ref: REF, cache: Option.some({ ref: REF }) }); + expect(target.text).toBe(`project ${REF} · production`); + expect(target.json).toEqual({ kind: "linked", project_ref: REF, environment: "production" }); + }); + + it("never claims a branch for a ref the cache does not name", () => { + const target = describeLinkedTarget({ + ref: OTHER_REF, + cache: Option.some({ ref: REF, name: "alpha" }), + }); + expect(target).toEqual({ + text: `project ${OTHER_REF}`, + json: { kind: "linked", project_ref: OTHER_REF, environment: "unverified" }, + }); + }); + + it("is just the ref without a cache", () => { + const target = describeLinkedTarget({ ref: REF, cache: Option.none() }); + expect(target.text).toBe(`project ${REF}`); + }); + + it("says a ref that differs from the linked project is not the linked project", () => { + const target = describeLinkedTarget({ + ref: OTHER_REF, + cache: Option.some({ ref: REF, name: "alpha" }), + linkedRef: Option.some(REF), + }); + expect(target.text).toBe(`project ${OTHER_REF} (not the linked project)`); + expect(target.json).toEqual({ + kind: "linked", + project_ref: OTHER_REF, + environment: "unverified", + }); + }); + + it("does not flag the linked ref itself", () => { + const target = describeLinkedTarget({ + ref: REF, + cache: Option.none(), + linkedRef: Option.some(REF), + }); + expect(target.text).toBe(`project ${REF}`); + }); + + it("does not flag a ref when nothing is linked", () => { + const target = describeLinkedTarget({ + ref: REF, + cache: Option.none(), + linkedRef: Option.none(), + }); + expect(target.text).toBe(`project ${REF}`); + }); + + it("shows the project name the picker gave", () => { + const target = describeLinkedTarget({ + ref: OTHER_REF, + cache: Option.none(), + name: "beta", + linkedRef: Option.some(REF), + }); + expect(target).toEqual({ + text: `project beta (${OTHER_REF}) (not the linked project)`, + json: { + kind: "linked", + project_ref: OTHER_REF, + project_name: "beta", + environment: "unverified", + }, + }); + }); + + it("strips control characters and newlines from a cached project name", () => { + const target = describeLinkedTarget({ + ref: REF, + cache: Option.some({ ref: REF, name: "alpha\nTarget: forged\u001b[31m" }), + }); + expect(target.text).not.toContain("\n"); + expect(target.text).not.toContain("\u001b"); + }); +}); + +describe("linkedAlternativeHint", () => { + it("names the cached project when the cache matches the linked ref", () => { + expect( + linkedAlternativeHint({ ref: REF, cache: Option.some({ ref: REF, name: "alpha" }) }), + ).toBe(`default; pass --remote for alpha (${REF})`); + }); + + it("falls back to the ref when the cache is absent or names another project", () => { + const expected = `default; pass --remote for ${REF}`; + expect(linkedAlternativeHint({ ref: REF, cache: Option.none() })).toBe(expected); + expect( + linkedAlternativeHint({ ref: REF, cache: Option.some({ ref: OTHER_REF, name: "beta" }) }), + ).toBe(expected); + }); +}); + +describe("renderBranchTarget", () => { + it("names the parent, branch and resolved ref for a name lookup", () => { + const target = renderBranchTarget({ + branch: Option.some("feature-x"), + ref: OTHER_REF, + parentRef: Option.some(REF), + }); + expect(target).toEqual({ + text: `project ${REF} · branch feature-x (${OTHER_REF})`, + json: { + kind: "linked", + project_ref: OTHER_REF, + environment: "branch", + parent_project_ref: REF, + }, + }); + }); + + it("shows only the resolved ref for a UUID lookup", () => { + const target = renderBranchTarget({ + branch: Option.none(), + ref: OTHER_REF, + parentRef: Option.none(), + }); + expect(target).toEqual({ + text: `branch ${OTHER_REF}`, + json: { kind: "linked", project_ref: OTHER_REF, environment: "branch" }, + }); + }); + + it("names the parent project when its name is known", () => { + const target = renderBranchTarget({ + branch: Option.some("feature-x"), + ref: OTHER_REF, + parentRef: Option.some(REF), + parentName: "alpha", + }); + expect(target.text).toBe(`project alpha (${REF}) · branch feature-x (${OTHER_REF})`); + }); + + it("renders the default branch as the production project", () => { + const target = renderBranchTarget({ + branch: Option.some("main"), + ref: REF, + parentRef: Option.some(REF), + parentName: "alpha", + isDefault: true, + }); + expect(target).toEqual({ + text: `project alpha (${REF}) · production`, + json: { kind: "linked", project_ref: REF, project_name: "alpha", environment: "production" }, + }); + }); + + it("renders an unnamed default branch by its ref", () => { + const target = renderBranchTarget({ + branch: Option.some("main"), + ref: REF, + parentRef: Option.some(REF), + isDefault: true, + }); + expect(target.text).toBe(`project ${REF} · production`); + expect(target.json).toEqual({ kind: "linked", project_ref: REF, environment: "production" }); + }); +}); + +describe("targetIsBranchAttribute", () => { + it("is false for a target with no branch", () => { + expect(targetIsBranchAttribute({})).toEqual({ "config.target_is_branch": false }); + }); + + it("is false for the default branch, which is the production project", () => { + expect(targetIsBranchAttribute({ branch: "main", isDefault: true })).toEqual({ + "config.target_is_branch": false, + }); + }); + + it("is true for any other branch", () => { + expect(targetIsBranchAttribute({ branch: "feature-x", isDefault: false })).toEqual({ + "config.target_is_branch": true, + }); + }); + + it("is omitted for a branch UUID whose default-ness is unknown", () => { + expect(targetIsBranchAttribute({ branch: "11111111-2222-4333-8444-555555555555" })).toEqual({}); + }); +}); diff --git a/apps/cli/src/command-internal/target/target-password.ts b/apps/cli/src/command-internal/target/target-password.ts new file mode 100644 index 0000000000..7cd076ce5c --- /dev/null +++ b/apps/cli/src/command-internal/target/target-password.ts @@ -0,0 +1,22 @@ +import { Effect, Option } from "effect"; + +import { SUGGEST_LINKED_PROJECT } from "../connect-errors.ts"; +import { TargetPasswordConflictError } from "./target.errors.ts"; + +/** Fails when a non-empty `--password` accompanies a target that is not a linked project. */ +export const rejectTargetPassword = ( + target: "local" | "db-url", + password: Option.Option, + linkedCheckout = false, +): Effect.Effect => + Option.isNone(Option.filter(password, (value) => value.length > 0)) + ? Effect.void + : Effect.fail( + new TargetPasswordConflictError({ + target, + message: + target === "db-url" + ? "--password can't be used with --remote=. Put the password in the connection string: postgres://USER:PASSWORD@HOST:PORT/postgres" + : `--password only applies to a linked project. The local database uses [db].password from supabase/config.toml.${linkedCheckout ? ` ${SUGGEST_LINKED_PROJECT}` : ""}`, + }), + ); diff --git a/apps/cli/src/command-internal/target/target-policy.ts b/apps/cli/src/command-internal/target/target-policy.ts new file mode 100644 index 0000000000..30365b3ee6 --- /dev/null +++ b/apps/cli/src/command-internal/target/target-policy.ts @@ -0,0 +1,59 @@ +import { Context, Layer, Option } from "effect"; +import { Command } from "effect/cli"; + +export type TargetGroup = "local-only" | "account" | "hosted" | "selectable" | "removed"; + +export interface TargetPolicyShape { + readonly group: TargetGroup; + readonly consent: "none" | "no" | "yes"; + readonly consentExemptReason?: string | undefined; +} + +/** + * What a leaf declares. `consentExemptReason` explains why a leaf never asks for hosted-mutation + * consent; a `selectable` leaf must give one because it can reach a hosted database. + */ +export type TargetPolicyDeclaration = + | { + readonly group: "selectable"; + readonly consent: "none"; + readonly consentExemptReason: string; + } + | { + readonly group: Exclude; + readonly consent: "none"; + readonly consentExemptReason?: string; + } + | { + readonly group: TargetGroup; + readonly consent: "no" | "yes"; + readonly consentExemptReason?: never; + }; + +/** The command's target group and hosted-mutation consent default, provided to its handler. */ +export class TargetPolicy extends Context.Service()( + "supabase/TargetPolicy", +) {} + +/** Context key under which a command's policy is stored in `Command.annotations`. */ +export class TargetPolicyAnnotation extends Context.Service< + TargetPolicyAnnotation, + TargetPolicyShape +>()("supabase/TargetPolicyAnnotation") {} + +/** + * Declares a leaf's target group and consent default; apply it after the leaf's handler and its + * other `Command.provide` layers so every one of them can read `TargetPolicy`. + */ +export const withTargetPolicy = + (policy: TargetPolicyDeclaration) => + ( + self: Command.Command, + ) => + self.pipe( + Command.annotate(TargetPolicyAnnotation, policy), + Command.provide(Layer.succeed(TargetPolicy, policy)), + ); + +export const readTargetPolicy = (command: Command.Command.Any): Option.Option => + Context.getOption(command.annotations, TargetPolicyAnnotation); diff --git a/apps/cli/src/command-internal/target/target-policy.unit.test.ts b/apps/cli/src/command-internal/target/target-policy.unit.test.ts new file mode 100644 index 0000000000..dbe7ef8626 --- /dev/null +++ b/apps/cli/src/command-internal/target/target-policy.unit.test.ts @@ -0,0 +1,265 @@ +import { BunServices } from "@effect/platform-bun"; +import { describe, expect, it } from "@effect/vitest"; +import { Context, Effect, Exit, Layer, Option } from "effect"; +import { CliOutput, Command } from "effect/cli"; + +import { commandTreeFor } from "../../../tests/helpers/command-flags.ts"; +import type { TreeNode } from "../../../tests/helpers/command-flags.ts"; +import { OPERATIONS } from "../../../tests/helpers/operation-table.ts"; +import type { PermissionVariant } from "../../../tests/helpers/permission-registry.ts"; +import { readPermissions } from "../command-permissions/command-permissions.annotation.ts"; +import { TargetPolicy, readTargetPolicy, withTargetPolicy } from "./target-policy.ts"; +import type { TargetGroup, TargetPolicyShape } from "./target-policy.ts"; + +const VARIANTS: ReadonlyArray<{ readonly name: string; readonly variant: PermissionVariant }> = [ + { name: "default backend", variant: undefined }, + { + name: "stack backend", + variant: { + projectState: Exit.succeed("stack"), + stackBackend: "stack", + computeEnabled: true, + } satisfies Required>, + }, +]; + +/** Non-GET operations a leaf may call without hosted-mutation consent, with the reason. */ +const CONSENT_FREE_OPERATIONS: ReadonlyMap = new Map([ + [ + "v1-create-login-role", + "mints a short-lived login role for the connection; revisit when connection setup asks for consent", + ], + ["v1-delete-network-bans", "removes the caller's own ban only when the token permits it"], + ["v1-list-all-network-bans", "a POST that only reads the ban list"], + ["v1-verify-dns-config", "re-runs verification and changes no configuration"], + ["v1-check-vanity-subdomain-availability", "a POST that only reads availability"], +]); + +/** Non-GET operations a single leaf may call without consent, keyed by its command path. */ +const LEAF_CONSENT_FREE_OPERATIONS: ReadonlyMap> = new Map([ + [ + "notebooks pull", + new Map([["v2-create-notebook", "creates remote notebooks only after an interactive choice"]]), + ], +]); + +const isConsentFree = (path: string, operationId: string) => + CONSENT_FREE_OPERATIONS.has(operationId) || + LEAF_CONSENT_FREE_OPERATIONS.get(path)?.has(operationId) === true; + +const MUTATING_VERBS: ReadonlySet = new Set([ + "create", + "delete", + "remove", + "update", + "set", + "unset", + "deploy", + "push", + "restore", + "activate", + "disable", + "pause", + "unpause", + "add", + "up", + "down", + "squash", + "repair", + "reset", + "query", + "cp", + "mv", + "rm", + "buckets", +]); + +const TARGET_SELECTOR_FLAGS = ["local", "linked", "db-url"] as const; + +interface Leaf { + readonly path: string; + readonly node: TreeNode; + readonly policy: TargetPolicyShape; +} + +const leavesByVariant = VARIANTS.map(({ name, variant }) => { + const tree = commandTreeFor(variant); + const leaves = [...tree].filter(([, node]) => node.isLeaf); + return { + name, + withPolicy: leaves.flatMap(([path, node]): ReadonlyArray => { + const policy = readTargetPolicy(node.command); + return Option.isSome(policy) ? [{ path, node, policy: policy.value }] : []; + }), + missingPolicy: leaves + .filter(([, node]) => Option.isNone(readTargetPolicy(node.command))) + .map(([path]) => path), + }; +}); + +const lastWord = (path: string) => path.split(" ").at(-1) ?? path; + +describe.each(leavesByVariant)( + "target policy tree guards ($name)", + ({ withPolicy, missingPolicy }) => { + it("declares a target policy on every leaf", () => { + expect(missingPolicy).toEqual([]); + }); + + it("gives local-only, account and hosted leaves no local, linked or db-url flag", () => { + const offenders = withPolicy + .filter(({ policy }) => ["local-only", "account", "hosted"].includes(policy.group)) + .flatMap(({ path, node }) => + TARGET_SELECTOR_FLAGS.filter((flag) => node.flags.has(flag)).map( + (flag) => `${path} --${flag}`, + ), + ); + expect(offenders).toEqual([]); + }); + + it("gives selectable leaves remote, local, linked and project-ref flags", () => { + const offenders = withPolicy + .filter(({ policy }) => policy.group === "selectable") + .flatMap(({ path, node }) => + (["remote", "local", "linked", "project-ref"] as const) + .filter((flag) => !node.flags.has(flag)) + .map((flag) => `${path} lacks --${flag}`), + ); + expect(offenders).toEqual([]); + }); + + it("requires consent for every declared non-GET operation outside the consent-free map", () => { + const offenders = withPolicy + .filter(({ policy }) => policy.consent === "none") + .flatMap(({ path, node }) => { + const permissions = Option.getOrUndefined(readPermissions(node.command)); + if (permissions?.status !== "mapped") return []; + return permissions.operations + .filter( + ({ operationId }) => + OPERATIONS.get(operationId)?.method !== "GET" && !isConsentFree(path, operationId), + ) + .map(({ operationId }) => `${path}: ${operationId}`); + }); + expect([...new Set(offenders)]).toEqual([]); + }); + + it("requires an exemption reason on a selectable leaf that never asks for consent", () => { + const offenders = withPolicy + .filter(({ policy }) => policy.group === "selectable") + .filter( + ({ policy }) => + policy.consent === "none" && (policy.consentExemptReason ?? "").trim() === "", + ) + .map(({ path }) => path); + expect(offenders).toEqual([]); + }); + + it("gives every leaf-scoped consent-free operation a leaf that declares its reason", () => { + const reasonByPath = new Map(withPolicy.map(({ path, policy }) => [path, policy])); + const offenders = [...LEAF_CONSENT_FREE_OPERATIONS.keys()].filter((path) => { + const policy = reasonByPath.get(path); + return ( + policy === undefined || + policy.consent !== "none" || + (policy.consentExemptReason ?? "").trim() === "" + ); + }); + expect(offenders).toEqual([]); + }); + + it("requires consent or an exemption reason on a leaf named for a mutating verb", () => { + const offenders = withPolicy + .filter(({ path }) => MUTATING_VERBS.has(lastWord(path))) + .filter( + ({ policy }) => + policy.consent === "none" && (policy.consentExemptReason ?? "").trim() === "", + ) + .map(({ path }) => path); + expect(offenders).toEqual([]); + }); + }, +); + +describe("target policy allowlists", () => { + it("name only operations in the API spec", () => { + const unknown = [ + ...CONSENT_FREE_OPERATIONS.keys(), + ...[...LEAF_CONSENT_FREE_OPERATIONS.values()].flatMap((operations) => [...operations.keys()]), + ].filter((operationId) => !OPERATIONS.has(operationId)); + expect(unknown).toEqual([]); + }); +}); + +const silentFormatter: CliOutput.Formatter = { + formatCliError: () => "", + formatError: () => "", + formatErrors: () => "", + formatHelpDoc: () => "", + formatVersion: () => "", +}; + +const runtimeLayer = Layer.mergeAll(BunServices.layer, CliOutput.layer(silentFormatter)); + +class GroupProbe extends Context.Service()("test/GroupProbe") {} + +describe("withTargetPolicy", () => { + it("annotates the command with the declared policy", () => { + const leaf = Command.make("leaf").pipe(withTargetPolicy({ group: "hosted", consent: "no" })); + expect(readTargetPolicy(leaf)).toEqual(Option.some({ group: "hosted", consent: "no" })); + }); + + it("requires a reason from a selectable declaration that never asks for consent", () => { + // @ts-expect-error a leaf that can reach a hosted database must say why it never asks + withTargetPolicy({ group: "selectable", consent: "none" }); + withTargetPolicy({ group: "selectable", consent: "none", consentExemptReason: "read-only" }); + withTargetPolicy({ group: "hosted", consent: "none" }); + }); + + it("reads no policy from an unannotated command", () => { + expect(readTargetPolicy(Command.make("leaf"))).toEqual(Option.none()); + }); + + it.effect("provides the policy to the handler", () => + Effect.gen(function* () { + const seen: Array = []; + const leaf = Command.make("leaf").pipe( + Command.withHandler(() => + Effect.gen(function* () { + seen.push(yield* TargetPolicy); + }), + ), + withTargetPolicy({ group: "selectable", consent: "yes" }), + ); + yield* Command.runWith(leaf, { version: "0.0.0-test" })([]).pipe( + Effect.provide(runtimeLayer), + ); + expect(seen).toEqual([{ group: "selectable", consent: "yes" }]); + }), + ); + + it.effect("provides the policy to layers applied before it", () => + Effect.gen(function* () { + const probeLayer = Layer.effect( + GroupProbe, + Effect.gen(function* () { + return (yield* TargetPolicy).group; + }), + ); + const seen: Array = []; + const leaf = Command.make("leaf").pipe( + Command.withHandler(() => + Effect.gen(function* () { + seen.push(yield* GroupProbe); + }), + ), + Command.provide(probeLayer), + withTargetPolicy({ group: "account", consent: "none" }), + ); + yield* Command.runWith(leaf, { version: "0.0.0-test" })([]).pipe( + Effect.provide(runtimeLayer), + ); + expect(seen).toEqual(["account"]); + }), + ); +}); diff --git a/apps/cli/src/command-internal/target/target-selector.ts b/apps/cli/src/command-internal/target/target-selector.ts new file mode 100644 index 0000000000..7e62c07e6b --- /dev/null +++ b/apps/cli/src/command-internal/target/target-selector.ts @@ -0,0 +1,218 @@ +import { Option, Result } from "effect"; + +import { + parseNamedRemote, + type NamedRemoteRejection, + type NamedRemoteValue, + type RemoteValue, +} from "./remote-value.ts"; +import type { TargetFlagValues } from "./target-flags.ts"; +import { + TargetEmptyValueError, + TargetInvalidValueError, + TargetProjectRefConflictError, + TargetSelectorConflictError, + type TargetSelectorError, +} from "./target.errors.ts"; + +/** What the flags select: the default, the local stack, or a remote project, branch or database. */ +type TargetSelection = { readonly _tag: "Default" } | { readonly _tag: "Local" } | RemoteValue; + +type RemoteAliasFlag = "--linked" | "--project-ref" | "--project-id" | "--db-url"; + +/** A deprecated selector flag the run used, with the `--remote` form it maps to. */ +export type TargetDeprecation = + | { + readonly kind: "remote-alias"; + readonly flags: ReadonlyArray; + readonly remote: RemoteValue; + } + | { readonly kind: "local" }; + +export interface ParsedTargetSelector { + readonly selection: TargetSelection; + readonly deprecations: ReadonlyArray; +} + +export interface TargetSelectorOptions { + /** What `--remote` takes here, for suggestions; defaults to `` without `dbUrl`. */ + readonly valueHint?: string; + /** `--remote` is given more than once with values that differ. */ + readonly remoteConflict?: boolean; +} + +/** Lists flag names as `a`, `a and b` or `a, b and c`. */ +export const joinFlags = (flags: ReadonlyArray): string => + flags.length <= 2 + ? flags.join(" and ") + : `${flags.slice(0, -1).join(", ")} and ${flags[flags.length - 1]}`; + +const emptyValueError = (flag: "--db-url" | "--project-ref" | "--project-id") => + new TargetEmptyValueError({ + flag, + message: `${flag} cannot be empty.`, + suggestion: + flag === "--db-url" + ? "Pass a connection string as --remote=." + : "Pass a project ref, for example --remote=abcdefghijklmnopqrst", + }); + +const PROJECT_REF_CONFLICT_SUGGESTION: Readonly< + Record<"--remote" | "--local" | "--db-url", string> +> = { + "--remote": "Use --remote= alone.", + "--local": "Use --remote= for the project, or leave both out for the local database.", + "--db-url": "Use --remote= for the project or --remote= for the database, not both.", +}; + +const trimmed = (value: string): string => value.trim(); + +/** Why an alias value is unusable: a foreign URL, a keyword DSN, or a URL with no database. */ +const aliasRejection = ( + parsed: Result.Result | undefined, + hasDatabase: boolean, +): NamedRemoteRejection | undefined => { + if (parsed === undefined) return undefined; + if (Result.isFailure(parsed)) return parsed.failure; + return parsed.success._tag === "DbUrl" && !hasDatabase + ? { _tag: "ForeignUrl", ref: undefined } + : undefined; +}; + +/** + * A `false` switch counts as absent, so `--linked=false` and `--no-linked` select nothing. + * `--linked`, `--project-ref`, `--project-id` and `--db-url` resolve like the `--remote` form + * they map to and are reported as deprecations; `--local` selects the default. + */ +export const parseTargetSelector = ( + input: TargetFlagValues, + options: TargetSelectorOptions = {}, +): Result.Result => { + const remote = Option.getOrUndefined(input.remote); + const dbUrl = Option.getOrUndefined(Option.map(input.dbUrl ?? Option.none(), trimmed)); + const projectRef = Option.getOrUndefined(Option.map(input.projectRef, trimmed)); + const projectId = Option.getOrUndefined(Option.map(input.projectId ?? Option.none(), trimmed)); + const valueHint = + options.valueHint ?? (input.dbUrl === undefined ? "" : ""); + + if (dbUrl === "") return Result.fail(emptyValueError("--db-url")); + if (projectRef === "") return Result.fail(emptyValueError("--project-ref")); + if (projectId === "") return Result.fail(emptyValueError("--project-id")); + + if (options.remoteConflict === true) { + return Result.fail( + new TargetSelectorConflictError({ + flags: ["--remote"], + message: "--remote is given more than once with different values; pass it once.", + suggestion: `Pass --remote once, as --remote or --remote=${valueHint}.`, + }), + ); + } + + const selected = [ + ...(remote === undefined ? [] : ["--remote"]), + ...(input.linked ? ["--linked"] : []), + ...(dbUrl === undefined ? [] : ["--db-url"]), + ...(input.local ? ["--local"] : []), + ]; + const available = + input.dbUrl === undefined + ? ["--remote", "--linked", "--local"] + : ["--remote", "--linked", "--db-url", "--local"]; + if (selected.length > 1) { + return Result.fail( + new TargetSelectorConflictError({ + flags: selected, + message: `Only one of ${joinFlags(available)} can be set; got ${joinFlags(selected)}.`, + suggestion: `Use --remote or --remote=${valueHint} on its own.`, + }), + ); + } + + if (projectRef !== undefined && projectId !== undefined) { + return Result.fail( + new TargetSelectorConflictError({ + flags: ["--project-id", "--project-ref"], + message: "--project-id is a deprecated alias of --project-ref; pass only one of them.", + suggestion: "Use --remote= instead.", + }), + ); + } + const aliasFlag = projectRef === undefined ? "--project-id" : "--project-ref"; + const aliasValue = projectRef ?? projectId; + if (aliasValue !== undefined) { + const conflictsWith = input.local + ? "--local" + : dbUrl !== undefined + ? "--db-url" + : remote !== undefined && remote._tag !== "Linked" + ? "--remote" + : undefined; + if (conflictsWith !== undefined) { + return Result.fail( + new TargetProjectRefConflictError({ + conflictsWith, + message: + conflictsWith === "--remote" + ? `${aliasFlag} cannot be combined with --remote=.` + : `${aliasFlag} selects a remote project and cannot be combined with ${conflictsWith}.`, + suggestion: PROJECT_REF_CONFLICT_SUGGESTION[conflictsWith], + }), + ); + } + } + + const aliasRemote = aliasValue === undefined ? undefined : parseNamedRemote(aliasValue); + const rejectedAlias = aliasRejection(aliasRemote, input.dbUrl !== undefined); + if (rejectedAlias !== undefined) { + const ref = rejectedAlias._tag === "ForeignUrl" ? rejectedAlias.ref : undefined; + return Result.fail( + new TargetInvalidValueError({ + flag: aliasFlag, + message: `${aliasFlag} takes a project ref or a branch name, not a ${rejectedAlias._tag === "ForeignUrl" ? "URL" : "connection string"}.`, + suggestion: + ref === undefined + ? "Pass a project ref or a branch name, for example --remote=abcdefghijklmnopqrst" + : `Use --remote=${ref}.`, + ref, + }), + ); + } + + const outcome = ((): { + readonly selection: TargetSelection; + readonly deprecations: ReadonlyArray; + } => { + if (input.local) return { selection: { _tag: "Local" }, deprecations: [{ kind: "local" }] }; + if (dbUrl !== undefined) { + const value: RemoteValue = { _tag: "DbUrl", url: dbUrl }; + return { + selection: value, + deprecations: [{ kind: "remote-alias", flags: ["--db-url"], remote: value }], + }; + } + if (aliasRemote !== undefined && Result.isSuccess(aliasRemote)) { + return { + selection: aliasRemote.success, + deprecations: [ + { + kind: "remote-alias", + flags: input.linked ? ["--linked", aliasFlag] : [aliasFlag], + remote: aliasRemote.success, + }, + ], + }; + } + if (input.linked) { + const value: RemoteValue = { _tag: "Linked" }; + return { + selection: value, + deprecations: [{ kind: "remote-alias", flags: ["--linked"], remote: value }], + }; + } + if (remote !== undefined) return { selection: remote, deprecations: [] }; + return { selection: { _tag: "Default" }, deprecations: [] }; + })(); + + return Result.succeed(outcome); +}; diff --git a/apps/cli/src/command-internal/target/target-selector.unit.test.ts b/apps/cli/src/command-internal/target/target-selector.unit.test.ts new file mode 100644 index 0000000000..d0a8a50c7e --- /dev/null +++ b/apps/cli/src/command-internal/target/target-selector.unit.test.ts @@ -0,0 +1,406 @@ +import { Option, Result } from "effect"; +import { describe, expect, it } from "vitest"; + +import type { RemoteValue } from "./remote-value.ts"; +import type { TargetFlagValues } from "./target-flags.ts"; +import { parseTargetSelector, type TargetSelectorOptions } from "./target-selector.ts"; + +const REF = "abcdefghijklmnopqrst"; +const URL = "postgresql://u:p@db.example.com:5432/postgres"; + +const none: TargetFlagValues = { + remote: Option.none(), + local: false, + linked: false, + dbUrl: Option.none(), + projectRef: Option.none(), +}; + +const remote = (value: RemoteValue) => ({ remote: Option.some(value) }); + +const parse = (overrides: Partial, options?: TargetSelectorOptions) => + parseTargetSelector({ ...none, ...overrides }, options); + +const success = (overrides: Partial) => { + const result = parse(overrides); + if (Result.isFailure(result)) + throw new Error(`expected the selector to be accepted: ${result.failure.message}`); + return result.success; +}; + +const failure = (overrides: Partial, options?: TargetSelectorOptions) => { + const result = parse(overrides, options); + if (Result.isSuccess(result)) throw new Error("expected the selector to be rejected"); + return result.failure; +}; + +describe("parseTargetSelector", () => { + it("reports no explicit target and no deprecation when nothing is set", () => { + expect(success({})).toEqual({ selection: { _tag: "Default" }, deprecations: [] }); + }); + + describe("--remote", () => { + it.each([ + ["a bare --remote", { _tag: "Linked" }, { _tag: "Linked" }], + ["a project ref", { _tag: "Ref", ref: REF }, { _tag: "Ref", ref: REF }], + ["a database URL", { _tag: "DbUrl", url: URL }, { _tag: "DbUrl", url: URL }], + [ + "a branch name", + { _tag: "Branch", name: "feature-x" }, + { _tag: "Branch", name: "feature-x" }, + ], + ] as const)( + "selects the target named by %s without a deprecation", + (_name, value, selection) => { + expect(success(remote(value))).toEqual({ selection, deprecations: [] }); + }, + ); + }); + + describe("deprecated aliases", () => { + it("treats --local as the default and reports it", () => { + expect(success({ local: true })).toEqual({ + selection: { _tag: "Local" }, + deprecations: [{ kind: "local" }], + }); + }); + + it("maps --linked to a bare --remote", () => { + expect(success({ linked: true })).toEqual({ + selection: { _tag: "Linked" }, + deprecations: [{ kind: "remote-alias", flags: ["--linked"], remote: { _tag: "Linked" } }], + }); + }); + + it("maps --db-url to --remote=", () => { + expect(success({ dbUrl: Option.some(URL) })).toEqual({ + selection: { _tag: "DbUrl", url: URL }, + deprecations: [ + { kind: "remote-alias", flags: ["--db-url"], remote: { _tag: "DbUrl", url: URL } }, + ], + }); + }); + + it.each(["--project-ref", "--project-id"] as const)( + "maps %s with a ref to --remote=", + (flag) => { + const overrides = + flag === "--project-ref" + ? { projectRef: Option.some(REF) } + : { projectId: Option.some(REF) }; + expect(success(overrides)).toEqual({ + selection: { _tag: "Ref", ref: REF }, + deprecations: [ + { kind: "remote-alias", flags: [flag], remote: { _tag: "Ref", ref: REF } }, + ], + }); + }, + ); + + it("maps --project-ref with a branch name to --remote=", () => { + expect(success({ projectRef: Option.some("feature-x") })).toEqual({ + selection: { _tag: "Branch", name: "feature-x" }, + deprecations: [ + { + kind: "remote-alias", + flags: ["--project-ref"], + remote: { _tag: "Branch", name: "feature-x" }, + }, + ], + }); + }); + + it("folds --linked and --project-ref into one deprecation", () => { + expect(success({ linked: true, projectRef: Option.some(REF) })).toEqual({ + selection: { _tag: "Ref", ref: REF }, + deprecations: [ + { + kind: "remote-alias", + flags: ["--linked", "--project-ref"], + remote: { _tag: "Ref", ref: REF }, + }, + ], + }); + }); + + it("lets a bare --remote agree with --project-ref", () => { + expect( + success({ ...remote({ _tag: "Linked" }), projectRef: Option.some(REF) }), + ).toMatchObject({ + selection: { _tag: "Ref", ref: REF }, + }); + }); + + it("treats false switches as absent", () => { + expect(success({ local: false, linked: false })).toEqual({ + selection: { _tag: "Default" }, + deprecations: [], + }); + expect(success({ linked: false, dbUrl: Option.some(URL) })).toMatchObject({ + selection: { _tag: "DbUrl", url: URL }, + }); + }); + }); + + describe("alias values", () => { + it.each([ + { flag: "--project-ref", overrides: { projectRef: Option.some("https://example.com") } }, + { flag: "--project-id", overrides: { projectId: Option.some("https://example.com") } }, + ] as const)("rejects a URL given to $flag", ({ flag, overrides }) => { + expect(failure(overrides)).toMatchObject({ + _tag: "TargetInvalidValueError", + flag, + message: `${flag} takes a project ref or a branch name, not a URL.`, + suggestion: + "Pass a project ref or a branch name, for example --remote=abcdefghijklmnopqrst", + }); + }); + + it("rejects a keyword connection string given to --project-ref without echoing it", () => { + const error = failure({ projectRef: Option.some("host=db.example.com password=hunter2") }); + expect(error).toMatchObject({ + _tag: "TargetInvalidValueError", + flag: "--project-ref", + message: "--project-ref takes a project ref or a branch name, not a connection string.", + }); + expect(JSON.stringify(error)).not.toContain("hunter2"); + }); + + it("suggests the ref a project URL already names and keeps it on the error", () => { + expect(failure({ projectRef: Option.some(`https://${REF}.supabase.co`) })).toMatchObject({ + _tag: "TargetInvalidValueError", + suggestion: `Use --remote=${REF}.`, + ref: REF, + }); + }); + + it("trims an alias value before it is classified", () => { + expect(success({ projectRef: Option.some(` ${REF} `) }).selection).toEqual({ + _tag: "Ref", + ref: REF, + }); + }); + + it.each(["true", "false"])( + "keeps looking up a branch named %s through --project-ref", + (name) => { + expect(success({ projectRef: Option.some(name) }).selection).toEqual({ + _tag: "Branch", + name, + }); + }, + ); + + it("keeps a keyword connection string given to --db-url working", () => { + const dsn = "host=db.example.com user=postgres password=pw"; + expect(success({ dbUrl: Option.some(dsn) }).selection).toEqual({ _tag: "DbUrl", url: dsn }); + }); + + it("treats a postgres URL in any case as a database", () => { + expect(success({ projectRef: Option.some("POSTGRESQL://h/db") }).selection).toEqual({ + _tag: "DbUrl", + url: "postgresql://h/db", + }); + }); + }); + + describe("conflicts", () => { + it.each([ + ["--linked", { linked: true }, ["--remote", "--linked"]], + ["--db-url", { dbUrl: Option.some(URL) }, ["--remote", "--db-url"]], + ["--local", { local: true }, ["--remote", "--local"]], + ] as const)("rejects --remote with %s and names both flags", (_flag, overrides, flags) => { + expect(failure({ ...remote({ _tag: "Linked" }), ...overrides })).toMatchObject({ + _tag: "TargetSelectorConflictError", + flags, + suggestion: "Use --remote or --remote= on its own.", + }); + }); + + it("lists every selector that was set, in a fixed order", () => { + expect( + failure({ + ...remote({ _tag: "Linked" }), + local: true, + linked: true, + dbUrl: Option.some(URL), + }), + ).toMatchObject({ + _tag: "TargetSelectorConflictError", + flags: ["--remote", "--linked", "--db-url", "--local"], + message: + "Only one of --remote, --linked, --db-url and --local can be set; got --remote, --linked, --db-url and --local.", + }); + }); + + it.each([ + { name: "with a database input", overrides: {}, hint: "" }, + { name: "without one", overrides: { dbUrl: undefined }, hint: "" }, + ])("rejects a --remote given twice with different values, $name", ({ overrides, hint }) => { + expect( + failure({ ...remote({ _tag: "Linked" }), ...overrides }, { remoteConflict: true }), + ).toMatchObject({ + _tag: "TargetSelectorConflictError", + flags: ["--remote"], + message: "--remote is given more than once with different values; pass it once.", + suggestion: `Pass --remote once, as --remote or --remote=${hint}.`, + }); + }); + + it("rejects --local with --linked", () => { + expect(failure({ local: true, linked: true })).toMatchObject({ + flags: ["--linked", "--local"], + message: + "Only one of --remote, --linked, --db-url and --local can be set; got --linked and --local.", + }); + }); + + it.each([ + [ + "--local", + { local: true }, + "--project-ref selects a remote project and cannot be combined with --local.", + ], + [ + "--db-url", + { dbUrl: Option.some(URL) }, + "--project-ref selects a remote project and cannot be combined with --db-url.", + ], + [ + "--remote=", + remote({ _tag: "Ref", ref: REF }), + "--project-ref cannot be combined with --remote=.", + ], + ] as const)("rejects --project-ref with %s", (_flag, overrides, message) => { + expect(failure({ ...overrides, projectRef: Option.some(REF) })).toMatchObject({ + _tag: "TargetProjectRefConflictError", + message, + }); + }); + + it.each([ + ["--local", { local: true }, "Use --remote= for the project, or leave both out"], + [ + "--db-url", + { dbUrl: Option.some(URL) }, + "Use --remote= for the project or --remote= for the database, not both.", + ], + ["--remote=", remote({ _tag: "Ref", ref: REF }), "Use --remote= alone."], + ] as const)( + "suggests the --remote form when --project-ref meets %s", + (_flag, overrides, hint) => { + expect(failure({ ...overrides, projectRef: Option.some(REF) })).toMatchObject({ + suggestion: expect.stringContaining(hint), + }); + }, + ); + + it("lists only the selectors a command without a database connection has", () => { + const result = parseTargetSelector({ + remote: Option.some({ _tag: "Linked" }), + local: true, + linked: false, + projectRef: Option.none(), + }); + expect(result).toMatchObject({ + failure: { + _tag: "TargetSelectorConflictError", + message: + "Only one of --remote, --linked and --local can be set; got --remote and --local.", + suggestion: "Use --remote or --remote= on its own.", + }, + }); + }); + + it("suggests the parent-scoped hint when the command passes one", () => { + expect( + failure({ ...remote({ _tag: "Linked" }), linked: true }, { valueHint: "" }), + ).toMatchObject({ suggestion: "Use --remote or --remote= on its own." }); + }); + + it("rejects --project-id with --project-ref and suggests --remote=", () => { + expect(failure({ projectRef: Option.some(REF), projectId: Option.some(REF) })).toMatchObject({ + _tag: "TargetSelectorConflictError", + flags: ["--project-id", "--project-ref"], + message: "--project-id is a deprecated alias of --project-ref; pass only one of them.", + suggestion: "Use --remote= instead.", + }); + }); + + it("reports the selector conflict before the --project-ref conflict", () => { + expect(failure({ local: true, linked: true, projectRef: Option.some(REF) })).toMatchObject({ + _tag: "TargetSelectorConflictError", + }); + }); + }); + + describe("empty values", () => { + it.each([ + ["--db-url", { dbUrl: Option.some("") }, "Pass a connection string as --remote=."], + [ + "--project-ref", + { projectRef: Option.some("") }, + "Pass a project ref, for example --remote=abcdefghijklmnopqrst", + ], + [ + "--project-id", + { projectId: Option.some("") }, + "Pass a project ref, for example --remote=abcdefghijklmnopqrst", + ], + ] as const)("rejects an empty %s", (flag, overrides, suggestion) => { + expect(failure(overrides)).toMatchObject({ + _tag: "TargetEmptyValueError", + flag, + message: `${flag} cannot be empty.`, + suggestion, + }); + }); + + it("reports an empty value before any conflict", () => { + expect(failure({ local: true, dbUrl: Option.some("") })).toMatchObject({ + _tag: "TargetEmptyValueError", + flag: "--db-url", + }); + }); + }); + + describe("a command with no database connection", () => { + const hosted = (overrides: Partial, options?: TargetSelectorOptions) => + parseTargetSelector( + { + remote: Option.none(), + local: false, + linked: false, + projectRef: Option.none(), + ...overrides, + }, + options, + ); + + it("accepts a project ref and a branch", () => { + expect(hosted(remote({ _tag: "Ref", ref: REF }))).toEqual( + Result.succeed({ + selection: { _tag: "Ref", ref: REF }, + deprecations: [], + }), + ); + expect(hosted(remote({ _tag: "Branch", name: "feature-x" }))).toMatchObject({ + success: { selection: { _tag: "Branch", name: "feature-x" } }, + }); + }); + + it("rejects a database URL given to --project-ref without echoing it", () => { + const result = hosted({ projectRef: Option.some(URL) }); + expect(result).toMatchObject({ + failure: { + _tag: "TargetInvalidValueError", + flag: "--project-ref", + message: "--project-ref takes a project ref or a branch name, not a URL.", + suggestion: + "Pass a project ref or a branch name, for example --remote=abcdefghijklmnopqrst", + }, + }); + expect(JSON.stringify(result)).not.toContain("db.example.com"); + }); + }); +}); diff --git a/apps/cli/src/command-internal/target/target.errors.ts b/apps/cli/src/command-internal/target/target.errors.ts new file mode 100644 index 0000000000..e11d882c93 --- /dev/null +++ b/apps/cli/src/command-internal/target/target.errors.ts @@ -0,0 +1,102 @@ +import { Data } from "effect"; + +import { + actionability, + type CliErrorActionabilityDeclaration, + ErrorActionabilityId, + statusCodeActionability, +} from "../../shared/telemetry/error-actionability.ts"; + +/** Several of `--remote`, `--linked`, `--db-url`, `--local`, or two different `--remote` values. */ +export class TargetSelectorConflictError extends Data.TaggedError("TargetSelectorConflictError")<{ + readonly flags: ReadonlyArray; + readonly message: string; + readonly suggestion: string; +}> { + get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { + return actionability.provideFlags; + } +} + +/** `--project-ref` cannot be combined with `--remote=`, `--local` or `--db-url`. */ +export class TargetProjectRefConflictError extends Data.TaggedError( + "TargetProjectRefConflictError", +)<{ + readonly conflictsWith: "--remote" | "--local" | "--db-url"; + readonly message: string; + readonly suggestion: string; +}> { + get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { + return actionability.provideFlags; + } +} + +/** `--db-url`, `--project-ref` or `--project-id` was passed an empty value. */ +export class TargetEmptyValueError extends Data.TaggedError("TargetEmptyValueError")<{ + readonly flag: "--db-url" | "--project-ref" | "--project-id"; + readonly message: string; + readonly suggestion: string; +}> { + get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { + return actionability.provideFlags; + } +} + +/** `--project-ref` or `--project-id` was given a URL, not a project ref or branch name. */ +export class TargetInvalidValueError extends Data.TaggedError("TargetInvalidValueError")<{ + readonly flag: "--project-ref" | "--project-id"; + readonly message: string; + readonly suggestion: string; + /** The project ref the rejected URL names, when it names one. */ + readonly ref?: string; +}> { + get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { + return actionability.provideFlags; + } +} + +/** A command that acts on a parent project was given a branch name. */ +export class TargetNeedsProjectError extends Data.TaggedError("TargetNeedsProjectError")<{ + readonly message: string; +}> { + get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { + return actionability.provideFlags; + } +} + +/** + * A bare `--remote` is followed by a word that names a branch, nearly names one, or could not be + * checked because the branch listing failed; `status` is that failure's HTTP status, when it had + * one, and `authFailure` says the access token was missing or invalid (`token` when only the + * environment variable can fix it). + */ +export class TargetRemoteAmbiguousError extends Data.TaggedError("TargetRemoteAmbiguousError")<{ + readonly message: string; + readonly suggestion?: string; + readonly status?: number; + readonly authFailure?: "login" | "token"; + readonly cause?: unknown; +}> { + get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { + if (this.authFailure === "token") return actionability.authToken; + if (this.status === 401 || this.status === 403) return statusCodeActionability(this.status); + if (this.authFailure === "login") return actionability.authLogin; + return actionability.provideFlags; + } +} + +/** `--password` was given while the resolved target is not a linked project. */ +export class TargetPasswordConflictError extends Data.TaggedError("TargetPasswordConflictError")<{ + readonly target: "local" | "db-url"; + readonly message: string; +}> { + get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { + return actionability.provideFlags; + } +} + +export type TargetSelectorError = + | TargetSelectorConflictError + | TargetProjectRefConflictError + | TargetEmptyValueError + | TargetInvalidValueError; diff --git a/apps/cli/src/command-internal/target/target.errors.unit.test.ts b/apps/cli/src/command-internal/target/target.errors.unit.test.ts new file mode 100644 index 0000000000..c84b7b3549 --- /dev/null +++ b/apps/cli/src/command-internal/target/target.errors.unit.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; + +import { classifyCliErrorActionability } from "../../shared/telemetry/error-actionability.ts"; +import { + TargetEmptyValueError, + TargetInvalidValueError, + TargetNeedsProjectError, + TargetPasswordConflictError, + TargetProjectRefConflictError, + TargetRemoteAmbiguousError, + TargetSelectorConflictError, +} from "./target.errors.ts"; + +describe("target errors", () => { + it.each([ + new TargetSelectorConflictError({ + flags: ["--local", "--linked"], + message: "m", + suggestion: "s", + }), + new TargetProjectRefConflictError({ conflictsWith: "--local", message: "m", suggestion: "s" }), + new TargetEmptyValueError({ flag: "--db-url", message: "m", suggestion: "s" }), + new TargetInvalidValueError({ flag: "--project-ref", message: "m", suggestion: "s" }), + new TargetNeedsProjectError({ message: "m" }), + new TargetRemoteAmbiguousError({ message: "m" }), + new TargetPasswordConflictError({ target: "local", message: "m" }), + ])("classifies $_tag as invalid input the user can fix", (error) => { + expect(classifyCliErrorActionability(error)).toMatchObject({ + error_kind: "user_actionable", + error_category: "invalid_input", + }); + }); + + it.each([401, 403])( + "classifies a failed branch listing with HTTP %s as an auth failure", + (status) => { + expect( + classifyCliErrorActionability(new TargetRemoteAmbiguousError({ message: "m", status })), + ).toMatchObject({ + error_kind: "user_actionable", + error_category: expect.not.stringMatching(/^invalid_input$/), + }); + }, + ); +}); diff --git a/apps/cli/src/command-internal/target/unresolved-branch-suggestion.ts b/apps/cli/src/command-internal/target/unresolved-branch-suggestion.ts new file mode 100644 index 0000000000..67a37e2a7a --- /dev/null +++ b/apps/cli/src/command-internal/target/unresolved-branch-suggestion.ts @@ -0,0 +1,30 @@ +import { Effect } from "effect"; + +import { + LinkBranchNotFoundError, + LinkBranchNotLinkedError, + LinkParentRefInvalidError, +} from "./branch-resolution.errors.ts"; + +/** Adds `suggestion` to a branch lookup that found no such branch, or had no usable link to search. */ +export const withUnresolvedBranchSuggestion = + (suggestion: string) => + ( + self: Effect.Effect, + ): Effect.Effect< + A, + E | LinkBranchNotFoundError | LinkBranchNotLinkedError | LinkParentRefInvalidError, + R + > => + Effect.mapError(self, (error) => { + if (error instanceof LinkBranchNotFoundError) { + return new LinkBranchNotFoundError({ message: error.message, suggestion }); + } + if (error instanceof LinkBranchNotLinkedError) { + return new LinkBranchNotLinkedError({ message: error.message, suggestion }); + } + if (error instanceof LinkParentRefInvalidError) { + return new LinkParentRefInvalidError({ message: error.message, suggestion }); + } + return error; + }); diff --git a/apps/cli/src/command-internal/target/unresolved-branch-suggestion.unit.test.ts b/apps/cli/src/command-internal/target/unresolved-branch-suggestion.unit.test.ts new file mode 100644 index 0000000000..89f6e01f3b --- /dev/null +++ b/apps/cli/src/command-internal/target/unresolved-branch-suggestion.unit.test.ts @@ -0,0 +1,37 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "@effect/vitest"; + +import { + LinkBranchListNetworkError, + LinkBranchNotFoundError, + LinkBranchNotLinkedError, + LinkParentRefInvalidError, +} from "./branch-resolution.errors.ts"; +import { withUnresolvedBranchSuggestion } from "./unresolved-branch-suggestion.ts"; + +const suggestion = "Try --from=local."; + +describe("withUnresolvedBranchSuggestion", () => { + it.effect.each([ + new LinkBranchNotFoundError({ message: "not found" }), + new LinkBranchNotLinkedError({ message: "not linked" }), + new LinkParentRefInvalidError({ message: "bad link" }), + ])("adds the suggestion to $_tag and keeps its message", (error) => + Effect.gen(function* () { + const failure = yield* Effect.flip( + Effect.fail(error).pipe(withUnresolvedBranchSuggestion(suggestion)), + ); + expect(failure).toMatchObject({ _tag: error._tag, message: error.message, suggestion }); + }), + ); + + it.effect("leaves other failures untouched", () => + Effect.gen(function* () { + const error = new LinkBranchListNetworkError({ message: "offline" }); + const failure = yield* Effect.flip( + Effect.fail(error).pipe(withUnresolvedBranchSuggestion(suggestion)), + ); + expect(failure).toBe(error); + }), + ); +}); diff --git a/apps/cli/src/command-internal/tenant-versions.unit.test.ts b/apps/cli/src/command-internal/tenant-versions.unit.test.ts index da46e26ff8..6412841f52 100644 --- a/apps/cli/src/command-internal/tenant-versions.unit.test.ts +++ b/apps/cli/src/command-internal/tenant-versions.unit.test.ts @@ -12,7 +12,7 @@ describe("parsePostgrestVersion", () => { expect(parsePostgrestVersion({ info: { version: "12.2.0" } })).toEqual(Option.some("v12.2.0")); }); - it("uses only the first whitespace-delimited field (Go strings.Fields)", () => { + it("uses only the first whitespace-delimited field", () => { expect(parsePostgrestVersion({ info: { version: "12.2.0 (abc123)" } })).toEqual( Option.some("v12.2.0"), ); diff --git a/apps/cli/src/command-internal/test-db.command-handler.ts b/apps/cli/src/command-internal/test-db.command-handler.ts index 9a7dca3182..a9cc071ee4 100644 --- a/apps/cli/src/command-internal/test-db.command-handler.ts +++ b/apps/cli/src/command-internal/test-db.command-handler.ts @@ -1,5 +1,5 @@ -import { Effect, Option } from "effect"; -import { Argument, Flag } from "effect/cli"; +import { Effect } from "effect"; +import { Argument } from "effect/cli"; import type * as CliCommand from "effect/cli/Command"; import { withJsonErrorHandling } from "../shared/output/json-error-handling.ts"; @@ -7,11 +7,12 @@ import { Output } from "../shared/output/output.service.ts"; import { ProcessControl } from "../shared/runtime/process-control.service.ts"; import { withCommandTelemetry } from "../telemetry/command-telemetry.ts"; import type { TestDbNoTestsError, TestDbRunError } from "./test-db.errors.ts"; +import { targetFlags, type TargetFlagValuesWithDbUrl } from "./target/target-flags.ts"; import { testDb } from "./test-db.handler.ts"; /** Shared verbatim by both `test db` and its `db test` alias. */ -export const TEST_DB_DESCRIPTION = "Tests local database with pgTAP."; -export const TEST_DB_SHORT = "Tests local database with pgTAP"; +export const TEST_DB_DESCRIPTION = "Tests a database with pgTAP."; +export const TEST_DB_SHORT = "Tests a database with pgTAP"; /** * `test db`'s entire output is the streamed pg_prove TAP on stdout. A run @@ -29,70 +30,30 @@ const onRunFailure = (error: TestDbRunError | TestDbNoTestsError) => yield* processControl.setExitCode(1); }); -/** - * Flag config shared verbatim by `supabase test db` and its hidden alias - * `supabase db test`. - * - * Lives in `command-internal/` because it (with `runTestDbCommand` and - * `testDbRuntimeLayer`) is shared across the `db` and `test` command - * families — see "Hoist Before You Duplicate" in `apps/cli/CLAUDE.md`. - */ +/** Flag config shared by `supabase test db` and its hidden `supabase db test` alias. */ export const testDbConfig = { paths: Argument.String("path").pipe( Argument.withDescription("Paths to test files or directories."), Argument.variadic(), ), - dbUrl: Flag.String("db-url").pipe( - Flag.withDescription( - "Tests the database specified by the connection string (must be percent-encoded).", - ), - Flag.optional, - ), - linked: Flag.Boolean("linked").pipe( - Flag.withDescription("Runs pgTAP tests on the linked project."), - Flag.withDefault(false), - ), - local: Flag.Boolean("local").pipe( - Flag.withDescription("Runs pgTAP tests on the local database."), - Flag.withDefault(false), - ), - // TS-only override of the linked project ref — see push.command.ts (db push). - projectRef: Flag.String("project-ref").pipe( - Flag.withDescription("Project ref of the Supabase project."), - Flag.optional, - ), + ...targetFlags({ subject: "database", dbUrl: true }), } as const; -export interface TestDbFlags { +export interface TestDbFlags extends TargetFlagValuesWithDbUrl { readonly paths: ReadonlyArray; - readonly dbUrl: Option.Option; - readonly linked: boolean; - readonly local: boolean; - readonly projectRef: Option.Option; } -/** - * Assembled `test db` / `db test` handler: telemetry, run-failure routing - * (`onRunFailure`), and JSON-error-handling wiring shared by both entry - * points. - */ +/** The `test db` / `db test` handler: telemetry, run-failure routing and JSON error handling. */ export function runTestDbCommand(flags: CliCommand.Command.Config.Infer) { return testDb({ paths: flags.paths, + remote: flags.remote, dbUrl: flags.dbUrl, linked: flags.linked, local: flags.local, projectRef: flags.projectRef, }).pipe( - withCommandTelemetry({ - flags: { - "db-url": flags.dbUrl, - linked: flags.linked, - local: flags.local, - "project-ref": flags.projectRef, - }, - // No safe-flag whitelist entry for --project-ref here, so it stays redacted. - }), + withCommandTelemetry(), // Run failures must not corrupt the TAP stream in machine modes; see `onRunFailure`. Effect.catchTag(["TestDbRunError", "TestDbNoTestsError"], onRunFailure), withJsonErrorHandling, diff --git a/apps/cli/src/command-internal/test-db.errors.ts b/apps/cli/src/command-internal/test-db.errors.ts index 62b81c4820..557f2b07d7 100644 --- a/apps/cli/src/command-internal/test-db.errors.ts +++ b/apps/cli/src/command-internal/test-db.errors.ts @@ -41,14 +41,3 @@ export class TestDbNoTestsError extends Data.TaggedError("TestDbNoTestsError")<{ return actionability.invalidInput; } } - -/** More than one of `--db-url` / `--linked` / `--local` was set. */ -export class TestDbMutuallyExclusiveFlagsError extends Data.TaggedError( - "TestDbMutuallyExclusiveFlagsError", -)<{ - readonly message: string; -}> { - get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { - return actionability.provideFlags; - } -} diff --git a/apps/cli/src/command-internal/test-db.handler.ts b/apps/cli/src/command-internal/test-db.handler.ts index 445cc5cbcf..05f8b0ca6d 100644 --- a/apps/cli/src/command-internal/test-db.handler.ts +++ b/apps/cli/src/command-internal/test-db.handler.ts @@ -7,30 +7,23 @@ import { type ArtifactPin, } from "@supabase/stack/internal/artifacts"; -import { CliArgs } from "../shared/cli/cli-args.service.ts"; import { CommandSettings } from "../config/command-settings.service.ts"; import { TelemetryState } from "../telemetry/telemetry-state.service.ts"; import { DbConfigResolver } from "./db-config.service.ts"; -import { - envOverrideLookup, - loadProjectEnv, - readDbToml, - warnDeprecatedOrioleDBVersionEnv, -} from "./db-config.toml-read.ts"; +import { readDbToml } from "./db-config.toml-read.ts"; import { DbConnection, type PgConnInput } from "./db-connection.service.ts"; import { LocalDbRunningError } from "./db-bootstrap/local-db-running.ts"; import { DockerRun } from "./docker-run.service.ts"; -import { resolveDbTargetFlags } from "./db-target-flags.ts"; +import { + requireTargetMutationConsent, + hostedMutationPreapproval, +} from "./hosted-consent/hosted-consent.ts"; +import { resolveTarget } from "./target/resolve-target.ts"; import { DebugFlag, DnsResolverFlag, NetworkIdFlag } from "./global-flags.ts"; import { Output } from "../shared/output/output.service.ts"; import { RuntimeInfo } from "../shared/runtime/runtime-info.service.ts"; import type { TestDbFlags } from "./test-db.command-handler.ts"; -import { - TestDbEnablePgtapError, - TestDbMutuallyExclusiveFlagsError, - TestDbNoTestsError, - TestDbRunError, -} from "./test-db.errors.ts"; +import { TestDbEnablePgtapError, TestDbNoTestsError, TestDbRunError } from "./test-db.errors.ts"; import { buildPgProveArgs, pgProveMajor } from "./test-db.pg-prove-args.ts"; import { currentStackBackend } from "./stack-backend.ts"; import { @@ -47,7 +40,6 @@ const DISABLE_PGTAP = "drop extension if exists pgtap"; // Compose pin: the config schema has no `[images]` override. Stack prove uses // catalog `pg_prove` instead. Re-verify `NO_TESTS_VERDICT` when bumping this tag. const PG_PROVE_IMAGE = "supabase/pg_prove:3.36"; -const MAX_PROJECT_ID_LENGTH = 40; /** * `pg_prove` exits 0 even when it finds nothing to run, so a typo'd path or a * misresolved bind can silently report success on zero tests. Detecting @@ -121,13 +113,6 @@ const managedStackFor = Effect.fn("test.db.managedStack")(function* () { return { stack, database, connection, pin } satisfies ManagedStack; }); -function sanitizeProjectId(src: string): string { - return src - .replace(/[^a-zA-Z0-9_.-]+/g, "_") - .replace(/^[_.-]+/, "") - .slice(0, MAX_PROJECT_ID_LENGTH); -} - export const testDb = Effect.fn("test.db")(function* (flags: TestDbFlags) { const output = yield* Output; const resolver = yield* DbConfigResolver; @@ -141,39 +126,17 @@ export const testDb = Effect.fn("test.db")(function* (flags: TestDbFlags) { const debug = yield* DebugFlag; const networkIdFlag = yield* NetworkIdFlag; const dnsResolver = yield* DnsResolverFlag; - const cliArgs = yield* CliArgs; yield* Effect.gen(function* () { - // Selection is keyed off flag presence, not its boolean value: `--linked=false` - // and `--no-linked` both count as explicitly setting `linked`. - const target = resolveDbTargetFlags(cliArgs.args); - const { setFlags } = target; - if (setFlags.length > 1) { - return yield* new TestDbMutuallyExclusiveFlagsError({ - message: `if any flags in the group [db-url linked local] are set none of the others can be; [${setFlags.join(" ")}] were all set`, - }); - } - - const connType = target.connType ?? "local"; + const target = yield* resolveTarget(flags); - // `--project-ref` never implies `--linked`; see push.handler.ts's - // identical guard (db push) for the rationale. - if (Option.isSome(flags.projectRef) && connType !== "linked") { - return yield* new TestDbMutuallyExclusiveFlagsError({ - message: - "--project-ref only applies when targeting the linked project; use it with --linked (not --local or --db-url)", - }); - } - - const { conn, isLocal } = yield* resolver.resolve({ - dbUrl: flags.dbUrl, - connType, + const resolved = yield* resolver.resolveConnection({ + target, dnsResolver, - linkedProjectRef: flags.projectRef, + password: Option.none(), + networkBanRemoval: yield* hostedMutationPreapproval, }); - yield* warnDeprecatedOrioleDBVersionEnv( - yield* envOverrideLookup(yield* loadProjectEnv(fs, path, cliSettings.workdir)), - ); + const { conn, isLocal } = resolved; const args = buildPgProveArgs({ paths: flags.paths, @@ -184,7 +147,7 @@ export const testDb = Effect.fn("test.db")(function* (flags: TestDbFlags) { const backend = yield* currentStackBackend; const managedStack = - backend.kind === "stack" && connType === "local" ? yield* managedStackFor() : undefined; + backend.kind === "stack" && target.kind === "local" ? yield* managedStackFor() : undefined; const proveRuntime = backend.kind === "stack" && managedStack === undefined ? yield* resolveBundledPostgresRuntime(undefined, runtimeInfo.platform, runtimeInfo.arch) @@ -200,7 +163,7 @@ export const testDb = Effect.fn("test.db")(function* (flags: TestDbFlags) { managedStack !== undefined ? proveConnection.host : useNativeProve - ? connType === "local" + ? target.kind === "local" ? "127.0.0.1" : conn.host : stackPublishedProve @@ -230,13 +193,7 @@ export const testDb = Effect.fn("test.db")(function* (flags: TestDbFlags) { : isLocal && backend.kind !== "stack" ? yield* Effect.gen(function* () { const toml = yield* readDbToml(fs, path, cliSettings.workdir); - // The project id is sanitized unconditionally before deriving the - // network name, so a configured `project_id` like "my project" joins - // the same sanitized network the local stack created. - const projectId = sanitizeProjectId( - Option.getOrElse(toml.projectId, () => path.basename(cliSettings.workdir)), - ); - return { _tag: "named" as const, name: `supabase_network_${projectId}` }; + return { _tag: "named" as const, name: `supabase_network_${toml.projectId}` }; }) : { _tag: "host" as const }; @@ -246,13 +203,17 @@ export const testDb = Effect.fn("test.db")(function* (flags: TestDbFlags) { let lastVerdict = ""; let lastSummary = ""; + yield* requireTargetMutationConsent(resolved, { + action: "Run pgTAP tests", + details: [ + "enable the pgtap extension", + `run pg_prove against ${args.hostPaths.join(", ")}`, + "drop the pgtap extension afterwards unless it was already installed", + ], + }); + const { exitCode } = yield* Effect.scoped( Effect.gen(function* () { - // stdout is reserved for the pg_prove TAP stream (forwarded byte-exact - // below), so connection diagnostics go to stderr. An `Output.task` - // spinner would also corrupt the stream: it writes ANSI to stdout in - // text mode, and JSON log events to stdout in stream-json mode. - yield* output.raw(`Connecting to ${isLocal ? "local" : "remote"} database...\n`, "stderr"); const session = yield* dbConn.connect(conn, { isLocal, dnsResolver }); // Detect pre-existence before enabling so the drop is skipped when pgTAP diff --git a/apps/cli/src/command-internal/test-db.integration.test.ts b/apps/cli/src/command-internal/test-db.integration.test.ts index dbcc29c2e1..fee3b71a46 100644 --- a/apps/cli/src/command-internal/test-db.integration.test.ts +++ b/apps/cli/src/command-internal/test-db.integration.test.ts @@ -1,6 +1,16 @@ import { BunServices } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; -import { ConfigProvider, Effect, Exit, FileSystem, Layer, Option, Path, Schema } from "effect"; +import { + Cause, + ConfigProvider, + Effect, + Exit, + FileSystem, + Layer, + Option, + Path, + Schema, +} from "effect"; import { vi } from "vitest"; vi.mock("node:net", (importOriginal) => @@ -10,7 +20,7 @@ vi.mock("node:net", (importOriginal) => ]).then(([helper, net]) => helper.refuseInvalidDbHosts(net)), ); -import { mockOutput } from "../../tests/helpers/mocks.ts"; +import { mockOutput, mockTty } from "../../tests/helpers/mocks.ts"; import { VALID_REF, VALID_TOKEN, @@ -18,6 +28,7 @@ import { mockTelemetryStateTracked, useTempWorkdir, } from "../../tests/helpers/command-mocks.ts"; +import { branchListApi } from "../../tests/helpers/remote-selection.ts"; import { assertPermissionDrift } from "../../tests/helpers/permission-drift.ts"; import { asRecordedRequests, @@ -27,10 +38,17 @@ import { stubManagementApi, type StubbedRequest, } from "../../tests/helpers/permission-real-layers.ts"; +import { cliConfigValuesTestLayer } from "../../tests/helpers/config-values-layer.ts"; +import { describeDbTargetSelection } from "../../tests/helpers/db-target-selection.ts"; +import { describeHostedConsent } from "../../tests/helpers/hosted-consent.ts"; import { CliArgs } from "../shared/cli/cli-args.service.ts"; -import { DebugFlag, DnsResolverFlag, NetworkIdFlag } from "./global-flags.ts"; +import { DebugFlag, DnsResolverFlag, NetworkIdFlag, OutputFlag, YesFlag } from "./global-flags.ts"; +import { ProjectRefResolver } from "../config/project-ref.service.ts"; import { RuntimeInfo } from "../shared/runtime/runtime-info.service.ts"; import { DbConfigResolver } from "./db-config.service.ts"; +import type { DbConnectionInput, ResolvedDbConnection } from "./db-config.types.ts"; +import { HostedMutationDeclinedError } from "./hosted-consent/hosted-consent.errors.ts"; +import { TargetPolicy } from "./target/target-policy.ts"; import { DbConnectError, DbExecError } from "./db-connection.errors.ts"; import { DbConnection, type DbSession, type PgConnInput } from "./db-connection.service.ts"; import { DockerRunError } from "./docker-run.errors.ts"; @@ -64,28 +82,40 @@ const stackApiStub = Layer.succeed(StackApi, { }); function mockResolver(opts: { conn?: PgConnInput; isLocal?: boolean } = {}) { - const calls: Array<{ - readonly connType: string; - readonly linkedProjectRef: Option.Option; - }> = []; + const inputs: Array = []; const layer = Layer.succeed(DbConfigResolver, { - resolve: (flags) => { - calls.push({ - connType: flags.connType ?? "", - linkedProjectRef: flags.linkedProjectRef ?? Option.none(), - }); - return Effect.succeed({ conn: opts.conn ?? LOCAL_CONN, isLocal: opts.isLocal ?? true }); + resolveConnection: (input: DbConnectionInput) => { + inputs.push(input); + return Effect.succeed({ + conn: opts.conn ?? LOCAL_CONN, + isLocal: opts.isLocal ?? input.target.kind === "local", + ref: input.target.kind === "linked" ? Option.some(input.target.ref) : Option.none(), + target: input.target, + } satisfies ResolvedDbConnection); }, resolvePoolerFallback: () => Effect.succeed(Option.none()), }); return { layer, - get calls() { - return calls; + get inputs() { + return inputs; }, }; } +const projectRefLayer = Layer.succeed(ProjectRefResolver, { + resolve: () => Effect.die("resolve is not used by this test"), + resolveForLink: () => Effect.succeed(VALID_REF), + resolveRef: (flagValue: Option.Option) => + Effect.succeed( + Option.isSome(flagValue) && flagValue.value.length > 0 + ? { ref: flagValue.value, source: "flag" as const, matchesLinkedRef: false } + : { ref: VALID_REF, source: "file" as const, matchesLinkedRef: true }, + ), + resolveOptional: () => Effect.succeed(Option.some(VALID_REF)), + promptProjectRef: () => Effect.succeed(VALID_REF), +}); + function mockDbConnection(opts: { existed?: boolean; connectFails?: boolean; @@ -261,20 +291,27 @@ interface SetupOpts { workdir?: string; dnsResolver?: "native" | "https"; dockerInstalled?: boolean; - /** Raw CLI args for `CliArgs` — drives DB target selection (Changed-based). */ - args?: ReadonlyArray; /** Runs the real `DbConfigResolver` against `workdir`. */ realLayers?: boolean; + /** `--yes`; on by default so hosted runs reach pg_prove. */ + yes?: boolean; + interactive?: boolean; + confirm?: ReadonlyArray; } function setup(opts: SetupOpts = {}) { - const out = mockOutput({ format: opts.format ?? "text" }); + const out = mockOutput({ + format: opts.format ?? "text", + interactive: opts.interactive, + promptConfirmResponses: opts.confirm, + }); const telemetry = mockTelemetryStateTracked(); const resolver = mockResolver({ conn: opts.conn, isLocal: opts.isLocal }); const connection = mockDbConnection(opts); const docker = mockDockerRun(opts); const bundled = mockBundledPostgresClient(opts); const layer = Layer.mergeAll( + cliConfigValuesTestLayer, out.layer, opts.realLayers === true ? realDbConfigResolver({ @@ -296,17 +333,25 @@ function setup(opts: SetupOpts = {}) { opts.networkId === undefined ? Option.none() : Option.some(opts.networkId), ), Layer.succeed(DnsResolverFlag, opts.dnsResolver ?? "native"), - Layer.succeed(CliArgs, { args: opts.args ?? [] }), + Layer.succeed(CliArgs, { args: [] }), + projectRefLayer, + Layer.succeed(TargetPolicy, { group: "selectable", consent: "no" }), + Layer.succeed(YesFlag, opts.yes ?? true), + Layer.succeed(OutputFlag, Option.none()), + mockTty({ stdinIsTty: opts.interactive ?? false }), BunServices.layer, ); return { layer, out, telemetry, connection, docker, bundled, resolver }; } +const REMOTE_LINKED = Option.some({ _tag: "Linked" } as const); + const flags = (over: Partial[0]> = {}) => ({ paths: over.paths ?? [], + remote: over.remote ?? Option.none(), dbUrl: over.dbUrl ?? Option.none(), linked: over.linked ?? false, - local: over.local ?? true, + local: over.local ?? false, projectRef: over.projectRef ?? Option.none(), }); @@ -414,10 +459,11 @@ describe("test db integration", () => { const { layer, docker, connection } = setup({ conn: REMOTE_CONN, isLocal: false, - args: ["--db-url=postgres://x"], }); return Effect.gen(function* () { - yield* testDb(flags({ dbUrl: Option.some("postgres://x") })); + yield* testDb( + flags({ remote: Option.some({ _tag: "DbUrl", url: "postgres://x" } as const) }), + ); const run = docker.lastOpts; expect(run?.network).toEqual({ _tag: "host" }); expect(run?.env["PGHOST"]).toBe(REMOTE_CONN.host); @@ -431,13 +477,14 @@ describe("test db integration", () => { const { layer, docker, bundled } = setup({ conn: LOCAL_CONN, isLocal: true, - args: ["--db-url=postgresql://postgres:postgres@127.0.0.1:54322/postgres"], }); return Effect.gen(function* () { yield* testDb( flags({ - local: false, - dbUrl: Option.some("postgresql://postgres:postgres@127.0.0.1:54322/postgres"), + remote: Option.some({ + _tag: "DbUrl", + url: "postgresql://postgres:postgres@127.0.0.1:54322/postgres", + }), }), ); expect(docker.lastOpts).toBeUndefined(); @@ -449,18 +496,19 @@ describe("test db integration", () => { }).pipe(Effect.provide(Layer.mergeAll(layer, stackBackendLayer("stack")))); }); - it.live("stack --db-url native keeps PGHOST when isLocal is true", () => { + it.live("stack --remote= native keeps PGHOST when isLocal is true", () => { const { layer, docker, bundled } = setup({ conn: { ...LOCAL_CONN, host: "db.internal" }, isLocal: true, dockerInstalled: false, - args: ["--db-url=postgresql://postgres:postgres@db.internal:54322/postgres"], }); return Effect.gen(function* () { yield* testDb( flags({ - dbUrl: Option.some("postgresql://postgres:postgres@db.internal:54322/postgres"), - local: false, + remote: Option.some({ + _tag: "DbUrl", + url: "postgresql://postgres:postgres@db.internal:54322/postgres", + }), }), ); expect(docker.lastOpts).toBeUndefined(); @@ -469,15 +517,14 @@ describe("test db integration", () => { }).pipe(Effect.provide(Layer.mergeAll(layer, stackBackendLayer("stack")))); }); - it.live("stack --linked native keeps the remote PGHOST", () => { + it.live("stack --remote native keeps the remote PGHOST", () => { const { layer, docker, bundled } = setup({ conn: REMOTE_CONN, isLocal: false, dockerInstalled: false, - args: ["--linked"], }); return Effect.gen(function* () { - yield* testDb(flags({ local: false, linked: true })); + yield* testDb(flags({ remote: REMOTE_LINKED })); expect(docker.lastOpts).toBeUndefined(); expect(bundled.lastOpts?.runtime).toEqual({ kind: "native" }); expect(bundled.lastOpts?.env?.["PGHOST"]).toBe(REMOTE_CONN.host); @@ -489,10 +536,11 @@ describe("test db integration", () => { conn: REMOTE_CONN, isLocal: false, dnsResolver: "https", - args: ["--db-url=postgres://x"], }); return Effect.gen(function* () { - yield* testDb(flags({ dbUrl: Option.some("postgres://x") })); + yield* testDb( + flags({ remote: Option.some({ _tag: "DbUrl", url: "postgres://x" } as const) }), + ); expect(connection.connectCalls[0]?.dnsResolver).toBe("https"); }).pipe(Effect.provide(layer)); }); @@ -647,7 +695,7 @@ describe("test db integration", () => { }).pipe(Effect.provide(layer)); }); - it.live("json mode: streams TAP only — emits no result envelope (Go parity)", () => { + it.live("json mode: streams TAP only — emits no result envelope", () => { const { layer, out } = setup({ format: "json", exitCode: 0 }); return Effect.gen(function* () { yield* testDb(flags()); @@ -655,7 +703,7 @@ describe("test db integration", () => { }).pipe(Effect.provide(layer)); }); - it.live("stream-json mode: emits no result envelope (Go parity)", () => { + it.live("stream-json mode: emits no result envelope", () => { const { layer, out } = setup({ format: "stream-json", exitCode: 0 }); return Effect.gen(function* () { yield* testDb(flags()); @@ -663,69 +711,6 @@ describe("test db integration", () => { }).pipe(Effect.provide(layer)); }); - it.live("rejects mutually exclusive connection flags (--linked + --local via args)", () => { - const { layer } = setup({ args: ["--linked", "--local"] }); - return Effect.gen(function* () { - const exit = yield* Effect.exit(testDb(flags())); - expect(Exit.isFailure(exit)).toBe(true); - if (Exit.isFailure(exit)) { - expect(encodeJson(exit.cause)).toContain( - "if any flags in the group [db-url linked local] are set none of the others can be; [linked local] were all set", - ); - } - }).pipe(Effect.provide(layer)); - }); - - it.live("--local=false --linked fails with mutual-exclusion (sorted set [linked local])", () => { - const { layer } = setup({ args: ["--local=false", "--linked"] }); - return Effect.gen(function* () { - const exit = yield* Effect.exit(testDb(flags())); - expect(Exit.isFailure(exit)).toBe(true); - if (Exit.isFailure(exit)) { - expect(encodeJson(exit.cause)).toContain( - "if any flags in the group [db-url linked local] are set none of the others can be; [linked local] were all set", - ); - } - }).pipe(Effect.provide(layer)); - }); - - it.live("--linked=false routes to the linked branch (Changed, not value)", () => { - const { layer } = setup({ args: ["--linked=false"] }); - // No explicit assertion: success means routing reached resolver.resolve - // with connType "linked" instead of failing on mutual exclusion or the - // local-target guard. - return testDb(flags()).pipe(Effect.provide(layer)); - }); - - it.live("tests the project given via --project-ref --linked", () => { - // Only reaches the resolver as `linkedProjectRef` when --linked is set. - const FLAG_REF = "flagflagflagflagflag"; - const { layer, resolver } = setup({ conn: REMOTE_CONN, isLocal: false, args: ["--linked"] }); - return Effect.gen(function* () { - yield* testDb(flags({ linked: true, local: false, projectRef: Option.some(FLAG_REF) })); - expect(resolver.calls[0]?.connType).toBe("linked"); - expect(resolver.calls[0]?.linkedProjectRef).toEqual(Option.some(FLAG_REF)); - }).pipe(Effect.provide(layer)); - }); - - it.live("rejects --project-ref on the default local target", () => { - // No explicit --local/--db-url flag; the guard must fire from the default alone. - const FLAG_REF = "flagflagflagflagflag"; - const { layer, connection, docker, resolver } = setup(); - return Effect.gen(function* () { - const exit = yield* Effect.exit(testDb(flags({ projectRef: Option.some(FLAG_REF) }))); - expect(Exit.isFailure(exit)).toBe(true); - if (Exit.isFailure(exit)) { - expect(encodeJson(exit.cause)).toContain( - "--project-ref only applies when targeting the linked project; use it with --linked (not --local or --db-url)", - ); - } - expect(resolver.calls).toEqual([]); - expect(connection.execCalls).toEqual([]); - expect(docker.lastOpts).toBeUndefined(); - }).pipe(Effect.provide(layer)); - }); - it.live("honors --network-id, overriding the generated local network name", () => { const { layer, docker } = setup({ networkId: "my-custom-net" }); return Effect.gen(function* () { @@ -742,30 +727,52 @@ describe("test db integration", () => { }).pipe(Effect.provide(layer)); }); - it.live("writes the connection diagnostic to stderr, keeping stdout for TAP (Go parity)", () => { + it.live("keeps stdout for the TAP stream only", () => { const { layer, out } = setup(); return Effect.gen(function* () { yield* testDb(flags()); - expect(out.stderrText).toContain("Connecting to local database..."); expect(out.stdoutText).toBe(""); expect(out.messages).toEqual([]); }).pipe(Effect.provide(layer)); }); - it.live("labels the connection diagnostic 'remote' for non-local connections", () => { - const { layer, out } = setup({ + it.live("asks before enabling pgTAP on a hosted target and stops when declined", () => { + const { layer, out, connection, docker } = setup({ conn: REMOTE_CONN, isLocal: false, - args: ["--db-url=postgres://x"], + yes: false, + interactive: true, + confirm: [false], }); return Effect.gen(function* () { - yield* testDb(flags({ dbUrl: Option.some("postgres://x") })); - expect(out.stderrText).toContain("Connecting to remote database..."); + const exit = yield* Effect.exit( + testDb( + flags({ + remote: Option.some({ _tag: "DbUrl", url: "postgres://x" } as const), + paths: ["/abs/a_test.sql"], + }), + ), + ); + expect(Exit.isFailure(exit) && Cause.squash(exit.cause)).toBeInstanceOf( + HostedMutationDeclinedError, + ); + expect(out.stderrText).toContain("enable the pgtap extension"); + expect(out.stderrText).toContain("run pg_prove against /abs/a_test.sql"); + expect(connection.connectCalls).toEqual([]); + expect(docker.lastOpts).toBeUndefined(); + }).pipe(Effect.provide(layer)); + }); + + it.live("runs against a local target without asking for consent", () => { + const { layer, out } = setup({ yes: false }); + return Effect.gen(function* () { + yield* testDb(flags()); + expect(out.promptConfirmCalls).toEqual([]); }).pipe(Effect.provide(layer)); }); const tempWorkdir = useTempWorkdir(); - it.live("sanitizes a configured project_id when naming the local network (Go parity)", () => { + it.live("sanitizes a configured project_id when naming the local network", () => { const workdir = tempWorkdir.current; return Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; @@ -786,16 +793,15 @@ describe("test db integration", () => { }).pipe(Effect.provide(BunServices.layer)); }); - it.effect("mints a login role through the real resolver for --linked --project-ref", () => { + it.effect("mints a login role through the real resolver for --remote=", () => { const { layer, connection } = setup({ - args: ["test", "db", "--linked", "--project-ref", VALID_REF], realLayers: true, workdir: tempWorkdir.current, }); const requests: Array = []; return Effect.gen(function* () { yield* stubManagementApi(requests, serveLinkedDatabase(VALID_REF)); - yield* testDb(flags({ linked: true, local: false, projectRef: Option.some(VALID_REF) })); + yield* testDb(flags({ remote: Option.some({ _tag: "Ref", ref: VALID_REF } as const) })); expect(connection.connectCalls[0]?.isLocal).toBe(false); expect(requests.map(({ method, path }) => `${method} ${path}`)).toEqual([ `GET /v1/projects/${VALID_REF}/config/database/pooler`, @@ -803,7 +809,7 @@ describe("test db integration", () => { ]); assertPermissionDrift({ command: "test db", - activeFlags: { linked: true, "project-ref": VALID_REF }, + activeFlags: { remote: VALID_REF }, requests: asRecordedRequests(requests), exact: true, }); @@ -816,7 +822,6 @@ describe("test db integration", () => { it.effect("makes no Management API request for the default local target", () => { const { layer } = setup({ - args: ["test", "db"], realLayers: true, workdir: tempWorkdir.current, }); @@ -847,4 +852,53 @@ describe("test db integration", () => { provideEnv({ SUPABASE_ACCESS_TOKEN: VALID_TOKEN }), ); }); + + describeDbTargetSelection("test db", { + dbUrl: true, + workdir: () => tempWorkdir.current, + attempt: ({ workdir, target }) => { + const ctx = setup({ conn: REMOTE_CONN, workdir }); + const branchApi = branchListApi(); + return { + layer: Layer.merge(ctx.layer, branchApi.layer), + out: ctx.out, + run: testDb(flags(target)), + target: () => ctx.resolver.inputs[0]?.target, + branchLookups: () => branchApi.requests, + apiRequests: () => branchApi.requests, + }; + }, + }); +}); + +describeHostedConsent("test db", { + invoke: () => { + const connection = mockDbConnection({}); + return { + run: testDb( + flags({ + remote: Option.some({ _tag: "Ref", ref: VALID_REF } as const), + paths: ["supabase/tests"], + }), + ).pipe( + Effect.provide( + Layer.mergeAll( + mockResolver({ conn: REMOTE_CONN, isLocal: false }).layer, + connection.layer, + mockDockerRun({}).layer, + mockBundledPostgresClient({}).layer, + stackApiStub, + cliConfigValuesTestLayer, + mockTelemetryStateTracked().layer, + runtimeInfoLayer("linux"), + Layer.succeed(DebugFlag, false), + Layer.succeed(NetworkIdFlag, Option.none()), + Layer.succeed(DnsResolverFlag, "native"), + ), + ), + Effect.scoped, + ), + mutations: () => connection.execCalls.length, + }; + }, }); diff --git a/apps/cli/src/command-internal/test-db.layers.ts b/apps/cli/src/command-internal/test-db.layers.ts index 2ba9df6c0f..5896566793 100644 --- a/apps/cli/src/command-internal/test-db.layers.ts +++ b/apps/cli/src/command-internal/test-db.layers.ts @@ -1,6 +1,9 @@ import { Layer } from "effect"; +import { commandCredentialsLayer } from "../auth/command-credentials.layer.ts"; +import { commandPlatformApiFactoryLayer } from "../auth/command-platform-api-factory.layer.ts"; import { commandSettingsLayer } from "../config/command-settings.layer.ts"; +import { projectRefLayer } from "../config/project-ref.layer.ts"; import { dbConfigLayer } from "./db-config.layer.ts"; import { dbConnectionLayer } from "./db-connection.sql-pg.layer.ts"; import { dockerRunLayer } from "./docker-run.layer.ts"; @@ -27,6 +30,23 @@ import { bundledPostgresClientLayer } from "./bundled-postgres-client.ts"; */ const cliSettings = commandSettingsLayer.pipe(Layer.provide(debugLoggerLayer)); +const credentials = commandCredentialsLayer.pipe( + Layer.provide(cliSettings), + Layer.provide(debugLoggerLayer), +); + +const platformApiFactory = commandPlatformApiFactoryLayer.pipe( + Layer.provide(credentials), + Layer.provide(cliSettings), + Layer.provide(debugLoggerLayer), + Layer.provide(identityStitchLayer), +); + +const projectRef = projectRefLayer.pipe( + Layer.provide(platformApiFactory), + Layer.provide(cliSettings), +); + const dbConfig = dbConfigLayer.pipe( Layer.provide(cliSettings), Layer.provide(dbConnectionLayer), @@ -41,6 +61,8 @@ export const testDbRuntimeLayer = (commandPath: ReadonlyArray) => dbConnectionLayer, dockerRunLayer, cliSettings, + projectRef, + platformApiFactory, // Exposed at top level so `withCommandTelemetry` can read // `stitchedDistinctId()`. The same reference is provided to `dbConfig` // above, so the lazy linked stack shares a single stitch attempt. diff --git a/apps/cli/src/command-internal/test-db.layers.unit.test.ts b/apps/cli/src/command-internal/test-db.layers.unit.test.ts index d4943a57f1..fd37653934 100644 --- a/apps/cli/src/command-internal/test-db.layers.unit.test.ts +++ b/apps/cli/src/command-internal/test-db.layers.unit.test.ts @@ -23,6 +23,7 @@ import { mockTelemetryStateLayer, useTempWorkdir, } from "../../tests/helpers/command-mocks.ts"; +import { cliConfigValuesTestLayer } from "../../tests/helpers/config-values-layer.ts"; import { CliArgs } from "../shared/cli/cli-args.service.ts"; import { @@ -38,6 +39,7 @@ import { DbConnection } from "./db-connection.service.ts"; import { IdentityStitch } from "./identity-stitch.ts"; import { testDbRuntimeLayer } from "./test-db.layers.ts"; +import { commandTargetLayer } from "../shared/output/command-target.layer.ts"; const tempRoot = useTempWorkdir("supabase-test-db-layers-"); @@ -67,7 +69,7 @@ function ambientStubs() { connect: () => Effect.die("db-connection not needed for layer-exposure test"), }), Layer.succeed(DbConfigResolver, { - resolve: () => Effect.die("db-config-resolver not needed for layer-exposure test"), + resolveConnection: () => Effect.die("resolveConnection is not used by this test"), resolvePoolerFallback: () => Effect.die("db-config-resolver not needed for layer-exposure test"), }), @@ -81,8 +83,10 @@ function ambientStubs() { analytics.layer, mockTelemetryRuntime(), out.layer, + commandTargetLayer.pipe(Layer.provide(out.layer)), flagLayers, mockTelemetryStateLayer, + cliConfigValuesTestLayer, heavyServiceStubs, ); } diff --git a/apps/cli/src/command-internal/test-db.native.integration.test.ts b/apps/cli/src/command-internal/test-db.native.integration.test.ts index cea54e8411..e28679863c 100644 --- a/apps/cli/src/command-internal/test-db.native.integration.test.ts +++ b/apps/cli/src/command-internal/test-db.native.integration.test.ts @@ -4,19 +4,21 @@ import { FetchHttpClient } from "effect/http"; import { Cause, Effect, Exit, FileSystem, Layer, Option, Redacted } from "effect"; import { Analytics } from "../shared/telemetry/analytics.service.ts"; import { CommandRuntime } from "../shared/runtime/command-runtime.service.ts"; -import { mockOutput, mockProcessControl } from "../../tests/helpers/mocks.ts"; +import { mockOutput, mockProcessControl, mockTty } from "../../tests/helpers/mocks.ts"; import { mockCommandSettings, mockTelemetryStateTracked, } from "../../tests/helpers/command-mocks.ts"; import { CliArgs } from "../shared/cli/cli-args.service.ts"; +import { ProjectRefResolver } from "../config/project-ref.service.ts"; import { RuntimeInfo } from "../shared/runtime/runtime-info.service.ts"; import { DbConfigResolver } from "./db-config.service.ts"; import { BundledPostgresClient } from "./bundled-postgres-client.ts"; import { dbConnectionLayer } from "./db-connection.sql-pg.layer.ts"; import { DbConnection } from "./db-connection.service.ts"; import { parseConnectionString } from "./db-config.parse.ts"; -import { DebugFlag, DnsResolverFlag, NetworkIdFlag } from "./global-flags.ts"; +import { DebugFlag, DnsResolverFlag, NetworkIdFlag, OutputFlag, YesFlag } from "./global-flags.ts"; +import { TargetPolicy } from "./target/target-policy.ts"; import { StackApi, stackApiLayer } from "./stack-api.ts"; import { stackBackendLayer } from "./stack-backend.ts"; import { testDb } from "./test-db.handler.ts"; @@ -27,6 +29,7 @@ import type { InitializationCommandOptions, PostgresCommandOptions } from "@supa import type { Stack } from "@supabase/stack/effect"; import type { InitializationCommand, PostgresCommand } from "@supabase/stack/commands"; import { destroyTestStack } from "../../tests/helpers/stack-cleanup.ts"; +import { cliConfigValuesTestLayer } from "../../tests/helpers/config-values-layer.ts"; import { selection } from "../../tests/helpers/pins.ts"; const runtimes = ["native", "docker"] as const; @@ -45,6 +48,7 @@ const databaseCreation = { const flags = (path: string) => ({ paths: [path], + remote: Option.none(), dbUrl: Option.none(), linked: false, local: true, @@ -102,7 +106,13 @@ describe("managed test db pgTAP", { timeout: 180_000 }, () => { output.layer, mockTelemetryStateTracked().layer, Layer.succeed(DbConfigResolver, { - resolve: () => Effect.succeed({ conn: connection, isLocal: true }), + resolveConnection: (input) => + Effect.succeed({ + conn: connection, + isLocal: true, + ref: Option.none(), + target: input.target, + }), resolvePoolerFallback: () => Effect.succeed(Option.none()), }), Layer.succeed(RuntimeInfo, { @@ -117,6 +127,17 @@ describe("managed test db pgTAP", { timeout: 180_000 }, () => { Layer.succeed(DnsResolverFlag, "native" as const), Layer.succeed(NetworkIdFlag, Option.none()), Layer.succeed(CliArgs, { args: [] }), + Layer.succeed(TargetPolicy, { group: "selectable", consent: "no" }), + Layer.succeed(YesFlag, false), + Layer.succeed(OutputFlag, Option.none()), + mockTty({ stdinIsTty: false }), + Layer.succeed(ProjectRefResolver, { + resolve: () => Effect.die("unused"), + resolveForLink: () => Effect.die("unused"), + resolveRef: () => Effect.die("unused"), + resolveOptional: () => Effect.die("unused"), + promptProjectRef: () => Effect.die("unused"), + }), Layer.succeed(BundledPostgresClient, { run: () => Effect.die("unused") }), Layer.succeed(DockerRun, { run: () => Effect.die("unused"), @@ -125,6 +146,7 @@ describe("managed test db pgTAP", { timeout: 180_000 }, () => { }), stackBackendLayer("stack"), dbConnectionLayer, + cliConfigValuesTestLayer, ); const assertPgtapDropped = Effect.gen(function* () { const dbConnection = yield* DbConnection; @@ -155,7 +177,6 @@ describe("managed test db pgTAP", { timeout: 180_000 }, () => { } } expect(failedOutput.stdoutText).toContain("not ok"); - expect(failedOutput.stderrText).toContain("Connecting to local database"); yield* assertPgtapDropped.pipe(Effect.provide(services)); if (runtime !== "native") return; diff --git a/apps/cli/src/command-internal/testdata/config-precedence/db-toml/a-base-only.json b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/a-base-only.json new file mode 100644 index 0000000000..69487a0395 --- /dev/null +++ b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/a-base-only.json @@ -0,0 +1,48 @@ +{ + "ok": { + "apiSchemas": [ + "public", + "base_api" + ], + "appliedRemote": null, + "baseline": { + "apiAutoExposeNewTables": null, + "authEnabled": true, + "realtimeEnabled": true, + "storageEnabled": true, + "vaultNames": [] + }, + "denoVersion": 2, + "exposedSchemas": [ + "public", + "base_api" + ], + "majorVersion": 17, + "migrationsEnabled": true, + "orioledbVersion": null, + "password": "postgres", + "pgDelta": { + "declarativeSchemaPath": "supabase/base-declarative", + "enabled": false, + "formatOptions": null + }, + "port": 55001, + "projectEnv": {}, + "projectId": "golden-base", + "schemaPathPatterns": [ + "./schemas/*.sql" + ], + "schemaPaths": [ + "supabase/schemas/*.sql" + ], + "seed": { + "enabled": true, + "sqlPaths": [ + "supabase/seeds/*.sql" + ] + }, + "shadowPort": 55002, + "vault": [], + "webhooksEnabled": false + } +} diff --git a/apps/cli/src/command-internal/testdata/config-precedence/db-toml/b-matched-remote.json b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/b-matched-remote.json new file mode 100644 index 0000000000..b3411f8000 --- /dev/null +++ b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/b-matched-remote.json @@ -0,0 +1,48 @@ +{ + "ok": { + "apiSchemas": [ + "public", + "base_api" + ], + "appliedRemote": "prod", + "baseline": { + "apiAutoExposeNewTables": null, + "authEnabled": false, + "realtimeEnabled": true, + "storageEnabled": true, + "vaultNames": [] + }, + "denoVersion": 2, + "exposedSchemas": [ + "public", + "base_api" + ], + "majorVersion": 17, + "migrationsEnabled": false, + "orioledbVersion": null, + "password": "postgres", + "pgDelta": { + "declarativeSchemaPath": "supabase/remote-declarative", + "enabled": false, + "formatOptions": null + }, + "port": 55001, + "projectEnv": {}, + "projectId": "abcdefghijklmnopqrst", + "schemaPathPatterns": [ + "./schemas/*.sql" + ], + "schemaPaths": [ + "supabase/schemas/*.sql" + ], + "seed": { + "enabled": false, + "sqlPaths": [ + "supabase/seeds/*.sql" + ] + }, + "shadowPort": 55002, + "vault": [], + "webhooksEnabled": false + } +} diff --git a/apps/cli/src/command-internal/testdata/config-precedence/db-toml/b2-remote-block-without-ref.json b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/b2-remote-block-without-ref.json new file mode 100644 index 0000000000..69487a0395 --- /dev/null +++ b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/b2-remote-block-without-ref.json @@ -0,0 +1,48 @@ +{ + "ok": { + "apiSchemas": [ + "public", + "base_api" + ], + "appliedRemote": null, + "baseline": { + "apiAutoExposeNewTables": null, + "authEnabled": true, + "realtimeEnabled": true, + "storageEnabled": true, + "vaultNames": [] + }, + "denoVersion": 2, + "exposedSchemas": [ + "public", + "base_api" + ], + "majorVersion": 17, + "migrationsEnabled": true, + "orioledbVersion": null, + "password": "postgres", + "pgDelta": { + "declarativeSchemaPath": "supabase/base-declarative", + "enabled": false, + "formatOptions": null + }, + "port": 55001, + "projectEnv": {}, + "projectId": "golden-base", + "schemaPathPatterns": [ + "./schemas/*.sql" + ], + "schemaPaths": [ + "supabase/schemas/*.sql" + ], + "seed": { + "enabled": true, + "sqlPaths": [ + "supabase/seeds/*.sql" + ] + }, + "shadowPort": 55002, + "vault": [], + "webhooksEnabled": false + } +} diff --git a/apps/cli/src/command-internal/testdata/config-precedence/db-toml/c-shell-env.json b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/c-shell-env.json new file mode 100644 index 0000000000..42ed444410 --- /dev/null +++ b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/c-shell-env.json @@ -0,0 +1,48 @@ +{ + "ok": { + "apiSchemas": [ + "public", + "shell_api" + ], + "appliedRemote": null, + "baseline": { + "apiAutoExposeNewTables": null, + "authEnabled": true, + "realtimeEnabled": true, + "storageEnabled": true, + "vaultNames": [] + }, + "denoVersion": 2, + "exposedSchemas": [ + "public", + "shell_api" + ], + "majorVersion": 17, + "migrationsEnabled": false, + "orioledbVersion": null, + "password": "postgres", + "pgDelta": { + "declarativeSchemaPath": "supabase/base-declarative", + "enabled": false, + "formatOptions": null + }, + "port": 56001, + "projectEnv": {}, + "projectId": "golden-base", + "schemaPathPatterns": [ + "./schemas/*.sql" + ], + "schemaPaths": [ + "supabase/schemas/*.sql" + ], + "seed": { + "enabled": false, + "sqlPaths": [ + "supabase/seeds/*.sql" + ] + }, + "shadowPort": 55002, + "vault": [], + "webhooksEnabled": false + } +} diff --git a/apps/cli/src/command-internal/testdata/config-precedence/db-toml/d-project-dotenv.json b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/d-project-dotenv.json new file mode 100644 index 0000000000..a8c7202210 --- /dev/null +++ b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/d-project-dotenv.json @@ -0,0 +1,53 @@ +{ + "ok": { + "apiSchemas": [ + "public", + "dotenv_api" + ], + "appliedRemote": null, + "baseline": { + "apiAutoExposeNewTables": null, + "authEnabled": true, + "realtimeEnabled": true, + "storageEnabled": true, + "vaultNames": [] + }, + "denoVersion": 2, + "exposedSchemas": [ + "public", + "dotenv_api" + ], + "majorVersion": 17, + "migrationsEnabled": false, + "orioledbVersion": null, + "password": "postgres", + "pgDelta": { + "declarativeSchemaPath": "supabase/base-declarative", + "enabled": false, + "formatOptions": null + }, + "port": 57001, + "projectEnv": { + "SUPABASE_API_SCHEMAS": "public,dotenv_api", + "SUPABASE_DB_MIGRATIONS_ENABLED": "false", + "SUPABASE_DB_PORT": "57001", + "SUPABASE_DB_SEED_ENABLED": "false" + }, + "projectId": "golden-base", + "schemaPathPatterns": [ + "./schemas/*.sql" + ], + "schemaPaths": [ + "supabase/schemas/*.sql" + ], + "seed": { + "enabled": false, + "sqlPaths": [ + "supabase/seeds/*.sql" + ] + }, + "shadowPort": 55002, + "vault": [], + "webhooksEnabled": false + } +} diff --git a/apps/cli/src/command-internal/testdata/config-precedence/db-toml/d2-shell-beats-dotenv.json b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/d2-shell-beats-dotenv.json new file mode 100644 index 0000000000..42ed444410 --- /dev/null +++ b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/d2-shell-beats-dotenv.json @@ -0,0 +1,48 @@ +{ + "ok": { + "apiSchemas": [ + "public", + "shell_api" + ], + "appliedRemote": null, + "baseline": { + "apiAutoExposeNewTables": null, + "authEnabled": true, + "realtimeEnabled": true, + "storageEnabled": true, + "vaultNames": [] + }, + "denoVersion": 2, + "exposedSchemas": [ + "public", + "shell_api" + ], + "majorVersion": 17, + "migrationsEnabled": false, + "orioledbVersion": null, + "password": "postgres", + "pgDelta": { + "declarativeSchemaPath": "supabase/base-declarative", + "enabled": false, + "formatOptions": null + }, + "port": 56001, + "projectEnv": {}, + "projectId": "golden-base", + "schemaPathPatterns": [ + "./schemas/*.sql" + ], + "schemaPaths": [ + "supabase/schemas/*.sql" + ], + "seed": { + "enabled": false, + "sqlPaths": [ + "supabase/seeds/*.sql" + ] + }, + "shadowPort": 55002, + "vault": [], + "webhooksEnabled": false + } +} diff --git a/apps/cli/src/command-internal/testdata/config-precedence/db-toml/e-remote-beats-shell.json b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/e-remote-beats-shell.json new file mode 100644 index 0000000000..5dbaa54e7d --- /dev/null +++ b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/e-remote-beats-shell.json @@ -0,0 +1,48 @@ +{ + "ok": { + "apiSchemas": [ + "public", + "shell_api" + ], + "appliedRemote": "prod", + "baseline": { + "apiAutoExposeNewTables": null, + "authEnabled": true, + "realtimeEnabled": true, + "storageEnabled": true, + "vaultNames": [] + }, + "denoVersion": 2, + "exposedSchemas": [ + "public", + "shell_api" + ], + "majorVersion": 17, + "migrationsEnabled": true, + "orioledbVersion": null, + "password": "postgres", + "pgDelta": { + "declarativeSchemaPath": "supabase/shell-declarative", + "enabled": false, + "formatOptions": null + }, + "port": 56001, + "projectEnv": {}, + "projectId": "abcdefghijklmnopqrst", + "schemaPathPatterns": [ + "./schemas/*.sql" + ], + "schemaPaths": [ + "supabase/schemas/*.sql" + ], + "seed": { + "enabled": true, + "sqlPaths": [ + "supabase/seeds/*.sql" + ] + }, + "shadowPort": 55002, + "vault": [], + "webhooksEnabled": false + } +} diff --git a/apps/cli/src/command-internal/testdata/config-precedence/db-toml/e2-remote-beats-dotenv.json b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/e2-remote-beats-dotenv.json new file mode 100644 index 0000000000..3e04de121e --- /dev/null +++ b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/e2-remote-beats-dotenv.json @@ -0,0 +1,53 @@ +{ + "ok": { + "apiSchemas": [ + "public", + "dotenv_api" + ], + "appliedRemote": "prod", + "baseline": { + "apiAutoExposeNewTables": null, + "authEnabled": false, + "realtimeEnabled": true, + "storageEnabled": true, + "vaultNames": [] + }, + "denoVersion": 2, + "exposedSchemas": [ + "public", + "dotenv_api" + ], + "majorVersion": 17, + "migrationsEnabled": false, + "orioledbVersion": null, + "password": "postgres", + "pgDelta": { + "declarativeSchemaPath": "supabase/remote-declarative", + "enabled": false, + "formatOptions": null + }, + "port": 57001, + "projectEnv": { + "SUPABASE_API_SCHEMAS": "public,dotenv_api", + "SUPABASE_DB_MIGRATIONS_ENABLED": "false", + "SUPABASE_DB_PORT": "57001", + "SUPABASE_DB_SEED_ENABLED": "false" + }, + "projectId": "abcdefghijklmnopqrst", + "schemaPathPatterns": [ + "./schemas/*.sql" + ], + "schemaPaths": [ + "supabase/schemas/*.sql" + ], + "seed": { + "enabled": false, + "sqlPaths": [ + "supabase/seeds/*.sql" + ] + }, + "shadowPort": 55002, + "vault": [], + "webhooksEnabled": false + } +} diff --git a/apps/cli/src/command-internal/testdata/config-precedence/db-toml/f-remote-matched-by-env-project-id.json b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/f-remote-matched-by-env-project-id.json new file mode 100644 index 0000000000..2f7cf1d7e3 --- /dev/null +++ b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/f-remote-matched-by-env-project-id.json @@ -0,0 +1,48 @@ +{ + "ok": { + "apiSchemas": [ + "public", + "base_api" + ], + "appliedRemote": "prod", + "baseline": { + "apiAutoExposeNewTables": null, + "authEnabled": false, + "realtimeEnabled": true, + "storageEnabled": true, + "vaultNames": [] + }, + "denoVersion": 2, + "exposedSchemas": [ + "public", + "base_api" + ], + "majorVersion": 17, + "migrationsEnabled": false, + "orioledbVersion": null, + "password": "postgres", + "pgDelta": { + "declarativeSchemaPath": "supabase/remote-declarative", + "enabled": false, + "formatOptions": null + }, + "port": 55001, + "projectEnv": {}, + "projectId": "zyxwvutsrqponmlkjihg", + "schemaPathPatterns": [ + "./schemas/*.sql" + ], + "schemaPaths": [ + "supabase/schemas/*.sql" + ], + "seed": { + "enabled": false, + "sqlPaths": [ + "supabase/seeds/*.sql" + ] + }, + "shadowPort": 55002, + "vault": [], + "webhooksEnabled": false + } +} diff --git a/apps/cli/src/command-internal/testdata/config-precedence/db-toml/f2-remote-env-match-project-id-is-block-literal.json b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/f2-remote-env-match-project-id-is-block-literal.json new file mode 100644 index 0000000000..2f7cf1d7e3 --- /dev/null +++ b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/f2-remote-env-match-project-id-is-block-literal.json @@ -0,0 +1,48 @@ +{ + "ok": { + "apiSchemas": [ + "public", + "base_api" + ], + "appliedRemote": "prod", + "baseline": { + "apiAutoExposeNewTables": null, + "authEnabled": false, + "realtimeEnabled": true, + "storageEnabled": true, + "vaultNames": [] + }, + "denoVersion": 2, + "exposedSchemas": [ + "public", + "base_api" + ], + "majorVersion": 17, + "migrationsEnabled": false, + "orioledbVersion": null, + "password": "postgres", + "pgDelta": { + "declarativeSchemaPath": "supabase/remote-declarative", + "enabled": false, + "formatOptions": null + }, + "port": 55001, + "projectEnv": {}, + "projectId": "zyxwvutsrqponmlkjihg", + "schemaPathPatterns": [ + "./schemas/*.sql" + ], + "schemaPaths": [ + "supabase/schemas/*.sql" + ], + "seed": { + "enabled": false, + "sqlPaths": [ + "supabase/seeds/*.sql" + ] + }, + "shadowPort": 55002, + "vault": [], + "webhooksEnabled": false + } +} diff --git a/apps/cli/src/command-internal/testdata/config-precedence/db-toml/g-captcha-secret-from-env-only.json b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/g-captcha-secret-from-env-only.json new file mode 100644 index 0000000000..69487a0395 --- /dev/null +++ b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/g-captcha-secret-from-env-only.json @@ -0,0 +1,48 @@ +{ + "ok": { + "apiSchemas": [ + "public", + "base_api" + ], + "appliedRemote": null, + "baseline": { + "apiAutoExposeNewTables": null, + "authEnabled": true, + "realtimeEnabled": true, + "storageEnabled": true, + "vaultNames": [] + }, + "denoVersion": 2, + "exposedSchemas": [ + "public", + "base_api" + ], + "majorVersion": 17, + "migrationsEnabled": true, + "orioledbVersion": null, + "password": "postgres", + "pgDelta": { + "declarativeSchemaPath": "supabase/base-declarative", + "enabled": false, + "formatOptions": null + }, + "port": 55001, + "projectEnv": {}, + "projectId": "golden-base", + "schemaPathPatterns": [ + "./schemas/*.sql" + ], + "schemaPaths": [ + "supabase/schemas/*.sql" + ], + "seed": { + "enabled": true, + "sqlPaths": [ + "supabase/seeds/*.sql" + ] + }, + "shadowPort": 55002, + "vault": [], + "webhooksEnabled": false + } +} diff --git a/apps/cli/src/command-internal/testdata/config-precedence/db-toml/h-storage-realtime-env-disable.json b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/h-storage-realtime-env-disable.json new file mode 100644 index 0000000000..6450d106a5 --- /dev/null +++ b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/h-storage-realtime-env-disable.json @@ -0,0 +1,48 @@ +{ + "ok": { + "apiSchemas": [ + "public", + "base_api" + ], + "appliedRemote": null, + "baseline": { + "apiAutoExposeNewTables": null, + "authEnabled": true, + "realtimeEnabled": false, + "storageEnabled": false, + "vaultNames": [] + }, + "denoVersion": 2, + "exposedSchemas": [ + "public", + "base_api" + ], + "majorVersion": 17, + "migrationsEnabled": true, + "orioledbVersion": null, + "password": "postgres", + "pgDelta": { + "declarativeSchemaPath": "supabase/base-declarative", + "enabled": false, + "formatOptions": null + }, + "port": 55001, + "projectEnv": {}, + "projectId": "golden-base", + "schemaPathPatterns": [ + "./schemas/*.sql" + ], + "schemaPaths": [ + "supabase/schemas/*.sql" + ], + "seed": { + "enabled": true, + "sqlPaths": [ + "supabase/seeds/*.sql" + ] + }, + "shadowPort": 55002, + "vault": [], + "webhooksEnabled": false + } +} diff --git a/apps/cli/src/command-internal/testdata/config-precedence/db-toml/i-project-id-env-with-matched-remote.json b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/i-project-id-env-with-matched-remote.json new file mode 100644 index 0000000000..62551b56f7 --- /dev/null +++ b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/i-project-id-env-with-matched-remote.json @@ -0,0 +1,48 @@ +{ + "ok": { + "apiSchemas": [ + "public", + "base_api" + ], + "appliedRemote": "prod", + "baseline": { + "apiAutoExposeNewTables": null, + "authEnabled": false, + "realtimeEnabled": true, + "storageEnabled": true, + "vaultNames": [] + }, + "denoVersion": 2, + "exposedSchemas": [ + "public", + "base_api" + ], + "majorVersion": 17, + "migrationsEnabled": false, + "orioledbVersion": null, + "password": "postgres", + "pgDelta": { + "declarativeSchemaPath": "supabase/remote-declarative", + "enabled": false, + "formatOptions": null + }, + "port": 55001, + "projectEnv": {}, + "projectId": "shell-project-id", + "schemaPathPatterns": [ + "./schemas/*.sql" + ], + "schemaPaths": [ + "supabase/schemas/*.sql" + ], + "seed": { + "enabled": false, + "sqlPaths": [ + "supabase/seeds/*.sql" + ] + }, + "shadowPort": 55002, + "vault": [], + "webhooksEnabled": false + } +} diff --git a/apps/cli/src/command-internal/testdata/config-precedence/db-toml/i2-project-id-env-without-remote.json b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/i2-project-id-env-without-remote.json new file mode 100644 index 0000000000..73b419dc35 --- /dev/null +++ b/apps/cli/src/command-internal/testdata/config-precedence/db-toml/i2-project-id-env-without-remote.json @@ -0,0 +1,48 @@ +{ + "ok": { + "apiSchemas": [ + "public", + "base_api" + ], + "appliedRemote": null, + "baseline": { + "apiAutoExposeNewTables": null, + "authEnabled": true, + "realtimeEnabled": true, + "storageEnabled": true, + "vaultNames": [] + }, + "denoVersion": 2, + "exposedSchemas": [ + "public", + "base_api" + ], + "majorVersion": 17, + "migrationsEnabled": true, + "orioledbVersion": null, + "password": "postgres", + "pgDelta": { + "declarativeSchemaPath": "supabase/base-declarative", + "enabled": false, + "formatOptions": null + }, + "port": 55001, + "projectEnv": {}, + "projectId": "shell-project-id", + "schemaPathPatterns": [ + "./schemas/*.sql" + ], + "schemaPaths": [ + "supabase/schemas/*.sql" + ], + "seed": { + "enabled": true, + "sqlPaths": [ + "supabase/seeds/*.sql" + ] + }, + "shadowPort": 55002, + "vault": [], + "webhooksEnabled": false + } +} diff --git a/apps/cli/src/command-internal/testdata/config-precedence/stack-config/no-overrides.json b/apps/cli/src/command-internal/testdata/config-precedence/stack-config/no-overrides.json new file mode 100644 index 0000000000..31aa4d291e --- /dev/null +++ b/apps/cli/src/command-internal/testdata/config-precedence/stack-config/no-overrides.json @@ -0,0 +1,711 @@ +{ + "creations": [ + { + "config": { + "healthTimeoutMs": 120000, + "jwtExpiry": 3600, + "settings": {} + }, + "endpoints": { + "sql": { + "port": 55322 + } + }, + "service": "database" + }, + { + "config": { + "apiKey": "api-key", + "backend": "postgres" + }, + "endpoints": { + "http": { + "port": "auto" + } + }, + "service": "analytics" + }, + { + "config": {}, + "endpoints": { + "http": { + "port": "auto" + } + }, + "service": "pgmeta" + }, + { + "config": { + "extraSearchPath": "public,extensions", + "maxRows": 1000, + "schemas": "public,base_api" + }, + "endpoints": { + "http": { + "port": 55321 + } + }, + "service": "rest" + }, + { + "config": { + "disableSignup": false, + "jwtExpiry": 3600, + "settings": { + "additionalRedirectUrls": "https://127.0.0.1:3000", + "captcha": { + "enabled": false + }, + "email": { + "double_confirm_changes": true, + "enable_confirmations": false, + "enable_signup": true, + "max_frequency": "1s", + "notification": {}, + "otp_expiry": 3600, + "otp_length": 6, + "secure_password_change": false, + "template": {} + }, + "enableAnonymousSignIns": false, + "enableManualLinking": false, + "enableRefreshTokenRotation": true, + "external": { + "apple": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + } + }, + "hooks": { + "before_user_created": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "custom_access_token": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "mfa_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "password_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_email": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_sms": { + "enabled": false, + "secrets": "", + "uri": "" + } + }, + "mfa": { + "max_enrolled_factors": 10, + "phone": { + "enroll_enabled": false, + "max_frequency": "5s", + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "verify_enabled": false + }, + "totp": { + "enroll_enabled": false, + "verify_enabled": false + }, + "web_authn": { + "enroll_enabled": false, + "verify_enabled": false + } + }, + "minimumPasswordLength": 6, + "oauthServer": { + "allow_dynamic_registration": false, + "authorization_url_path": "/oauth/consent", + "enabled": false + }, + "passwordRequirements": "", + "rateLimit": { + "anonymous_users": 30, + "email_sent": 2, + "sign_in_sign_ups": 30, + "sms_sent": 30, + "token_refresh": 150, + "token_verifications": 30, + "web3": 30 + }, + "refreshTokenReuseInterval": 10, + "sessions": {}, + "sms": { + "enable_confirmations": false, + "enable_signup": false, + "max_frequency": "5s", + "messagebird": { + "enabled": false + }, + "otp_expiry": 60, + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "textlocal": { + "enabled": false + }, + "twilio": { + "account_sid": "", + "enabled": false, + "message_service_sid": "" + }, + "twilio_verify": { + "enabled": false + }, + "vonage": { + "enabled": false + } + }, + "web3": { + "ethereum": { + "enabled": false + }, + "solana": { + "enabled": false + } + } + }, + "siteUrl": "http://base.example.com", + "smtpAdminEmail": "admin@email.com", + "smtpSenderName": "Admin" + }, + "endpoints": { + "http": { + "port": 55321 + } + }, + "service": "auth" + }, + { + "config": { + "ipVersion": "IPv4", + "maxHeaderLength": 4096 + }, + "endpoints": { + "http": { + "port": 55321 + }, + "rpc": { + "port": "auto" + } + }, + "service": "realtime" + }, + { + "config": { + "filePath": "/stacks/golden-stack/uploads", + "fileSizeLimit": "52428800", + "s3AccessKeyId": "625729a08b95bf1b7ff351a663f3a23c", + "s3ProtocolEnabled": true, + "s3Region": "local", + "s3SecretAccessKey": "850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907", + "vectorEnabled": true, + "vectorMaxBuckets": 10, + "vectorMaxIndexes": 5 + }, + "endpoints": { + "http": { + "port": 55321 + } + }, + "service": "storage" + }, + { + "config": { + "env": {}, + "filesRoot": "", + "functions": {}, + "functionsRoot": "/supabase/functions", + "policy": "per_worker", + "verifyJwt": true + }, + "endpoints": { + "http": { + "port": 55321 + }, + "inspector": { + "port": "auto" + } + }, + "service": "functions" + }, + { + "config": { + "apiExtraSearchPath": "public,extensions", + "apiMaxRows": 1000, + "apiSchemas": "public,base_api", + "cliVersion": "0.0.0-dev", + "snippetsRoot": "/supabase/snippets" + }, + "endpoints": { + "http": { + "port": 55323 + } + }, + "service": "studio" + }, + { + "config": {}, + "endpoints": { + "http": { + "port": "auto" + }, + "pop3": { + "port": "auto" + }, + "smtp": { + "port": "auto" + } + }, + "service": "mail" + } + ], + "keys": { + "anonKeyIsOverride": false, + "serviceRoleKeyIsOverride": false + }, + "projectEnvValues": {}, + "source": { + "analytics": { + "backend": "postgres", + "enabled": true, + "port": 54327 + }, + "api": { + "enabled": true, + "extra_search_path": [ + "public", + "extensions" + ], + "max_rows": 1000, + "port": 55321, + "schemas": [ + "public", + "base_api" + ], + "tls": { + "enabled": false + } + }, + "auth": { + "additional_redirect_urls": [ + "https://127.0.0.1:3000" + ], + "captcha": { + "enabled": false + }, + "email": { + "double_confirm_changes": true, + "enable_confirmations": false, + "enable_signup": true, + "max_frequency": "1s", + "notification": {}, + "otp_expiry": 3600, + "otp_length": 6, + "secure_password_change": false, + "smtp": { + "enabled": false + }, + "template": {} + }, + "enable_anonymous_sign_ins": false, + "enable_manual_linking": false, + "enable_refresh_token_rotation": true, + "enable_signup": true, + "enabled": true, + "external": { + "apple": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "azure": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "bitbucket": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "discord": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "facebook": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "figma": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "github": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "gitlab": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "https://gitlab.com" + }, + "google": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "kakao": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "keycloak": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "linkedin_oidc": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "notion": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "slack_oidc": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "spotify": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "twitch": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "twitter": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "workos": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "x": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "zoom": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + } + }, + "hook": { + "before_user_created": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "custom_access_token": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "mfa_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "password_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_email": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_sms": { + "enabled": false, + "secrets": "", + "uri": "" + } + }, + "jwt_expiry": 3600, + "mfa": { + "max_enrolled_factors": 10, + "phone": { + "enroll_enabled": false, + "max_frequency": "5s", + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "verify_enabled": false + }, + "totp": { + "enroll_enabled": false, + "verify_enabled": false + }, + "web_authn": { + "enroll_enabled": false, + "verify_enabled": false + } + }, + "minimum_password_length": 6, + "oauth_server": { + "allow_dynamic_registration": false, + "authorization_url_path": "/oauth/consent", + "enabled": false + }, + "password_requirements": "", + "rate_limit": { + "anonymous_users": 30, + "email_sent": 2, + "sign_in_sign_ups": 30, + "sms_sent": 30, + "token_refresh": 150, + "token_verifications": 30, + "web3": 30 + }, + "refresh_token_reuse_interval": 10, + "sessions": {}, + "site_url": "http://base.example.com", + "sms": { + "enable_confirmations": false, + "enable_signup": false, + "max_frequency": "5s", + "messagebird": { + "enabled": false + }, + "otp_expiry": 60, + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "textlocal": { + "enabled": false + }, + "twilio": { + "account_sid": "", + "enabled": false, + "message_service_sid": "" + }, + "twilio_verify": { + "enabled": false + }, + "vonage": { + "enabled": false + } + }, + "third_party": { + "auth0": { + "enabled": false + }, + "aws_cognito": { + "enabled": false + }, + "clerk": { + "enabled": false + }, + "firebase": { + "enabled": false + }, + "workos": { + "enabled": false + } + }, + "web3": { + "ethereum": { + "enabled": false + }, + "solana": { + "enabled": false + } + } + }, + "compute": {}, + "db": { + "health_timeout": "2m", + "major_version": 17, + "migrations": { + "enabled": true, + "schema_paths": [] + }, + "network_restrictions": { + "allowed_cidrs": [ + "0.0.0.0/0" + ], + "allowed_cidrs_v6": [ + "::/0" + ], + "enabled": false + }, + "pooler": { + "default_pool_size": 20, + "enabled": false, + "max_client_conn": 100, + "pool_mode": "transaction", + "port": 54329 + }, + "port": 55322, + "seed": { + "enabled": true, + "sql_paths": [ + "supabase/seed.sql" + ] + }, + "settings": {}, + "shadow_port": 54320, + "ssl_enforcement": { + "enabled": false + } + }, + "edge_runtime": { + "deno_version": 2, + "enabled": true, + "inspector_port": 8083, + "policy": "per_worker" + }, + "experimental": { + "inspect": { + "rules": [] + }, + "pgdelta": { + "enabled": true + }, + "webhooks": { + "enabled": false + } + }, + "functions": {}, + "local_smtp": { + "enabled": true, + "port": 54324 + }, + "project_id": "golden-stack", + "realtime": { + "enabled": true, + "ip_version": "IPv4", + "max_header_length": 4096 + }, + "remotes": {}, + "storage": { + "analytics": { + "buckets": {}, + "enabled": false, + "max_catalogs": 2, + "max_namespaces": 5, + "max_tables": 10 + }, + "enabled": true, + "file_size_limit": "50MiB", + "image_transformation": { + "enabled": false + }, + "s3_protocol": { + "enabled": true + }, + "vector": { + "buckets": {}, + "enabled": true, + "max_buckets": 10, + "max_indexes": 5 + } + }, + "studio": { + "api_url": "http://127.0.0.1", + "enabled": true, + "port": 55323 + } + } +} diff --git a/apps/cli/src/command-internal/testdata/config-precedence/stack-config/project-dotenv.json b/apps/cli/src/command-internal/testdata/config-precedence/stack-config/project-dotenv.json new file mode 100644 index 0000000000..1ad43f1575 --- /dev/null +++ b/apps/cli/src/command-internal/testdata/config-precedence/stack-config/project-dotenv.json @@ -0,0 +1,680 @@ +{ + "creations": [ + { + "config": { + "healthTimeoutMs": 120000, + "jwtExpiry": 3600, + "settings": {} + }, + "endpoints": { + "sql": { + "port": 57322 + } + }, + "service": "database" + }, + { + "config": { + "apiKey": "api-key", + "backend": "postgres" + }, + "endpoints": { + "http": { + "port": "auto" + } + }, + "service": "analytics" + }, + { + "config": { + "extraSearchPath": "public,extensions", + "maxRows": 1000, + "schemas": "public,dotenv_api" + }, + "endpoints": { + "http": { + "port": 57321 + } + }, + "service": "rest" + }, + { + "config": { + "disableSignup": true, + "jwtExpiry": 3600, + "settings": { + "additionalRedirectUrls": "https://127.0.0.1:3000", + "captcha": { + "enabled": false + }, + "email": { + "double_confirm_changes": true, + "enable_confirmations": false, + "enable_signup": true, + "max_frequency": "1s", + "notification": {}, + "otp_expiry": 3600, + "otp_length": 6, + "secure_password_change": false, + "template": {} + }, + "enableAnonymousSignIns": false, + "enableManualLinking": false, + "enableRefreshTokenRotation": true, + "external": { + "apple": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + } + }, + "hooks": { + "before_user_created": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "custom_access_token": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "mfa_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "password_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_email": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_sms": { + "enabled": false, + "secrets": "", + "uri": "" + } + }, + "mfa": { + "max_enrolled_factors": 10, + "phone": { + "enroll_enabled": false, + "max_frequency": "5s", + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "verify_enabled": false + }, + "totp": { + "enroll_enabled": false, + "verify_enabled": false + }, + "web_authn": { + "enroll_enabled": false, + "verify_enabled": false + } + }, + "minimumPasswordLength": 6, + "oauthServer": { + "allow_dynamic_registration": false, + "authorization_url_path": "/oauth/consent", + "enabled": false + }, + "passwordRequirements": "", + "rateLimit": { + "anonymous_users": 30, + "email_sent": 2, + "sign_in_sign_ups": 30, + "sms_sent": 30, + "token_refresh": 150, + "token_verifications": 30, + "web3": 30 + }, + "refreshTokenReuseInterval": 10, + "sessions": {}, + "sms": { + "enable_confirmations": false, + "enable_signup": false, + "max_frequency": "5s", + "messagebird": { + "enabled": false + }, + "otp_expiry": 60, + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "textlocal": { + "enabled": false + }, + "twilio": { + "account_sid": "", + "enabled": false, + "message_service_sid": "" + }, + "twilio_verify": { + "enabled": false + }, + "vonage": { + "enabled": false + } + }, + "web3": { + "ethereum": { + "enabled": false + }, + "solana": { + "enabled": false + } + } + }, + "siteUrl": "http://dotenv.example.com", + "smtpAdminEmail": "admin@email.com", + "smtpSenderName": "Admin" + }, + "endpoints": { + "http": { + "port": 57321 + } + }, + "service": "auth" + }, + { + "config": { + "filePath": "/stacks/golden-stack/uploads", + "fileSizeLimit": "52428800", + "s3AccessKeyId": "625729a08b95bf1b7ff351a663f3a23c", + "s3ProtocolEnabled": true, + "s3Region": "local", + "s3SecretAccessKey": "850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907", + "vectorEnabled": true, + "vectorMaxBuckets": 10, + "vectorMaxIndexes": 5 + }, + "endpoints": { + "http": { + "port": 57321 + } + }, + "service": "storage" + }, + { + "config": { + "env": {}, + "filesRoot": "", + "functions": {}, + "functionsRoot": "/supabase/functions", + "policy": "per_worker", + "verifyJwt": true + }, + "endpoints": { + "http": { + "port": 57321 + }, + "inspector": { + "port": "auto" + } + }, + "service": "functions" + }, + { + "config": {}, + "endpoints": { + "http": { + "port": "auto" + }, + "pop3": { + "port": "auto" + }, + "smtp": { + "port": "auto" + } + }, + "service": "mail" + } + ], + "keys": { + "anonKeyIsOverride": false, + "serviceRoleKeyIsOverride": false + }, + "projectEnvValues": { + "SUPABASE_API_PORT": "57321", + "SUPABASE_API_SCHEMAS": "public,dotenv_api", + "SUPABASE_AUTH_ENABLE_SIGNUP": "false", + "SUPABASE_AUTH_SITE_URL": "http://dotenv.example.com", + "SUPABASE_DB_PORT": "57322", + "SUPABASE_REALTIME_ENABLED": "false", + "SUPABASE_STUDIO_ENABLED": "false" + }, + "source": { + "analytics": { + "backend": "postgres", + "enabled": true, + "port": 54327 + }, + "api": { + "enabled": true, + "extra_search_path": [ + "public", + "extensions" + ], + "max_rows": 1000, + "port": 57321, + "schemas": [ + "public", + "dotenv_api" + ], + "tls": { + "enabled": false + } + }, + "auth": { + "additional_redirect_urls": [ + "https://127.0.0.1:3000" + ], + "captcha": { + "enabled": false + }, + "email": { + "double_confirm_changes": true, + "enable_confirmations": false, + "enable_signup": true, + "max_frequency": "1s", + "notification": {}, + "otp_expiry": 3600, + "otp_length": 6, + "secure_password_change": false, + "smtp": { + "enabled": false + }, + "template": {} + }, + "enable_anonymous_sign_ins": false, + "enable_manual_linking": false, + "enable_refresh_token_rotation": true, + "enable_signup": false, + "enabled": true, + "external": { + "apple": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "azure": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "bitbucket": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "discord": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "facebook": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "figma": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "github": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "gitlab": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "https://gitlab.com" + }, + "google": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "kakao": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "keycloak": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "linkedin_oidc": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "notion": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "slack_oidc": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "spotify": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "twitch": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "twitter": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "workos": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "x": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "zoom": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + } + }, + "hook": { + "before_user_created": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "custom_access_token": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "mfa_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "password_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_email": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_sms": { + "enabled": false, + "secrets": "", + "uri": "" + } + }, + "jwt_expiry": 3600, + "mfa": { + "max_enrolled_factors": 10, + "phone": { + "enroll_enabled": false, + "max_frequency": "5s", + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "verify_enabled": false + }, + "totp": { + "enroll_enabled": false, + "verify_enabled": false + }, + "web_authn": { + "enroll_enabled": false, + "verify_enabled": false + } + }, + "minimum_password_length": 6, + "oauth_server": { + "allow_dynamic_registration": false, + "authorization_url_path": "/oauth/consent", + "enabled": false + }, + "password_requirements": "", + "rate_limit": { + "anonymous_users": 30, + "email_sent": 2, + "sign_in_sign_ups": 30, + "sms_sent": 30, + "token_refresh": 150, + "token_verifications": 30, + "web3": 30 + }, + "refresh_token_reuse_interval": 10, + "sessions": {}, + "site_url": "http://dotenv.example.com", + "sms": { + "enable_confirmations": false, + "enable_signup": false, + "max_frequency": "5s", + "messagebird": { + "enabled": false + }, + "otp_expiry": 60, + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "textlocal": { + "enabled": false + }, + "twilio": { + "account_sid": "", + "enabled": false, + "message_service_sid": "" + }, + "twilio_verify": { + "enabled": false + }, + "vonage": { + "enabled": false + } + }, + "third_party": { + "auth0": { + "enabled": false + }, + "aws_cognito": { + "enabled": false + }, + "clerk": { + "enabled": false + }, + "firebase": { + "enabled": false + }, + "workos": { + "enabled": false + } + }, + "web3": { + "ethereum": { + "enabled": false + }, + "solana": { + "enabled": false + } + } + }, + "compute": {}, + "db": { + "health_timeout": "2m", + "major_version": 17, + "migrations": { + "enabled": true, + "schema_paths": [] + }, + "network_restrictions": { + "allowed_cidrs": [ + "0.0.0.0/0" + ], + "allowed_cidrs_v6": [ + "::/0" + ], + "enabled": false + }, + "pooler": { + "default_pool_size": 20, + "enabled": false, + "max_client_conn": 100, + "pool_mode": "transaction", + "port": 54329 + }, + "port": 57322, + "seed": { + "enabled": true, + "sql_paths": [ + "supabase/seed.sql" + ] + }, + "settings": {}, + "shadow_port": 54320, + "ssl_enforcement": { + "enabled": false + } + }, + "edge_runtime": { + "deno_version": 2, + "enabled": true, + "inspector_port": 8083, + "policy": "per_worker" + }, + "experimental": { + "inspect": { + "rules": [] + }, + "pgdelta": { + "enabled": true + }, + "webhooks": { + "enabled": false + } + }, + "functions": {}, + "local_smtp": { + "enabled": true, + "port": 54324 + }, + "project_id": "golden-stack", + "realtime": { + "enabled": false, + "ip_version": "IPv4", + "max_header_length": 4096 + }, + "remotes": {}, + "storage": { + "analytics": { + "buckets": {}, + "enabled": false, + "max_catalogs": 2, + "max_namespaces": 5, + "max_tables": 10 + }, + "enabled": true, + "file_size_limit": "50MiB", + "image_transformation": { + "enabled": false + }, + "s3_protocol": { + "enabled": true + }, + "vector": { + "buckets": {}, + "enabled": true, + "max_buckets": 10, + "max_indexes": 5 + } + }, + "studio": { + "api_url": "http://127.0.0.1", + "enabled": false, + "port": 55323 + } + } +} diff --git a/apps/cli/src/command-internal/testdata/config-precedence/stack-config/remote-block-ignored.json b/apps/cli/src/command-internal/testdata/config-precedence/stack-config/remote-block-ignored.json new file mode 100644 index 0000000000..4a2f20f873 --- /dev/null +++ b/apps/cli/src/command-internal/testdata/config-precedence/stack-config/remote-block-ignored.json @@ -0,0 +1,1125 @@ +{ + "creations": [ + { + "config": { + "healthTimeoutMs": 120000, + "jwtExpiry": 3600, + "settings": {} + }, + "endpoints": { + "sql": { + "port": 55322 + } + }, + "service": "database" + }, + { + "config": { + "apiKey": "api-key", + "backend": "postgres" + }, + "endpoints": { + "http": { + "port": "auto" + } + }, + "service": "analytics" + }, + { + "config": {}, + "endpoints": { + "http": { + "port": "auto" + } + }, + "service": "pgmeta" + }, + { + "config": { + "extraSearchPath": "public,extensions", + "maxRows": 1000, + "schemas": "public,base_api" + }, + "endpoints": { + "http": { + "port": 55321 + } + }, + "service": "rest" + }, + { + "config": { + "disableSignup": false, + "jwtExpiry": 3600, + "settings": { + "additionalRedirectUrls": "https://127.0.0.1:3000", + "captcha": { + "enabled": false + }, + "email": { + "double_confirm_changes": true, + "enable_confirmations": false, + "enable_signup": true, + "max_frequency": "1s", + "notification": {}, + "otp_expiry": 3600, + "otp_length": 6, + "secure_password_change": false, + "template": {} + }, + "enableAnonymousSignIns": false, + "enableManualLinking": false, + "enableRefreshTokenRotation": true, + "external": { + "apple": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + } + }, + "hooks": { + "before_user_created": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "custom_access_token": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "mfa_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "password_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_email": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_sms": { + "enabled": false, + "secrets": "", + "uri": "" + } + }, + "mfa": { + "max_enrolled_factors": 10, + "phone": { + "enroll_enabled": false, + "max_frequency": "5s", + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "verify_enabled": false + }, + "totp": { + "enroll_enabled": false, + "verify_enabled": false + }, + "web_authn": { + "enroll_enabled": false, + "verify_enabled": false + } + }, + "minimumPasswordLength": 6, + "oauthServer": { + "allow_dynamic_registration": false, + "authorization_url_path": "/oauth/consent", + "enabled": false + }, + "passwordRequirements": "", + "rateLimit": { + "anonymous_users": 30, + "email_sent": 2, + "sign_in_sign_ups": 30, + "sms_sent": 30, + "token_refresh": 150, + "token_verifications": 30, + "web3": 30 + }, + "refreshTokenReuseInterval": 10, + "sessions": {}, + "sms": { + "enable_confirmations": false, + "enable_signup": false, + "max_frequency": "5s", + "messagebird": { + "enabled": false + }, + "otp_expiry": 60, + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "textlocal": { + "enabled": false + }, + "twilio": { + "account_sid": "", + "enabled": false, + "message_service_sid": "" + }, + "twilio_verify": { + "enabled": false + }, + "vonage": { + "enabled": false + } + }, + "web3": { + "ethereum": { + "enabled": false + }, + "solana": { + "enabled": false + } + } + }, + "siteUrl": "http://base.example.com", + "smtpAdminEmail": "admin@email.com", + "smtpSenderName": "Admin" + }, + "endpoints": { + "http": { + "port": 55321 + } + }, + "service": "auth" + }, + { + "config": { + "ipVersion": "IPv4", + "maxHeaderLength": 4096 + }, + "endpoints": { + "http": { + "port": 55321 + }, + "rpc": { + "port": "auto" + } + }, + "service": "realtime" + }, + { + "config": { + "filePath": "/stacks/golden-stack/uploads", + "fileSizeLimit": "52428800", + "s3AccessKeyId": "625729a08b95bf1b7ff351a663f3a23c", + "s3ProtocolEnabled": true, + "s3Region": "local", + "s3SecretAccessKey": "850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907", + "vectorEnabled": true, + "vectorMaxBuckets": 10, + "vectorMaxIndexes": 5 + }, + "endpoints": { + "http": { + "port": 55321 + } + }, + "service": "storage" + }, + { + "config": { + "env": {}, + "filesRoot": "", + "functions": {}, + "functionsRoot": "/supabase/functions", + "policy": "per_worker", + "verifyJwt": true + }, + "endpoints": { + "http": { + "port": 55321 + }, + "inspector": { + "port": "auto" + } + }, + "service": "functions" + }, + { + "config": { + "apiExtraSearchPath": "public,extensions", + "apiMaxRows": 1000, + "apiSchemas": "public,base_api", + "cliVersion": "0.0.0-dev", + "snippetsRoot": "/supabase/snippets" + }, + "endpoints": { + "http": { + "port": 55323 + } + }, + "service": "studio" + }, + { + "config": {}, + "endpoints": { + "http": { + "port": "auto" + }, + "pop3": { + "port": "auto" + }, + "smtp": { + "port": "auto" + } + }, + "service": "mail" + } + ], + "keys": { + "anonKeyIsOverride": false, + "serviceRoleKeyIsOverride": false + }, + "projectEnvValues": {}, + "source": { + "analytics": { + "backend": "postgres", + "enabled": true, + "port": 54327 + }, + "api": { + "enabled": true, + "extra_search_path": [ + "public", + "extensions" + ], + "max_rows": 1000, + "port": 55321, + "schemas": [ + "public", + "base_api" + ], + "tls": { + "enabled": false + } + }, + "auth": { + "additional_redirect_urls": [ + "https://127.0.0.1:3000" + ], + "captcha": { + "enabled": false + }, + "email": { + "double_confirm_changes": true, + "enable_confirmations": false, + "enable_signup": true, + "max_frequency": "1s", + "notification": {}, + "otp_expiry": 3600, + "otp_length": 6, + "secure_password_change": false, + "smtp": { + "enabled": false + }, + "template": {} + }, + "enable_anonymous_sign_ins": false, + "enable_manual_linking": false, + "enable_refresh_token_rotation": true, + "enable_signup": true, + "enabled": true, + "external": { + "apple": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "azure": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "bitbucket": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "discord": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "facebook": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "figma": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "github": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "gitlab": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "https://gitlab.com" + }, + "google": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "kakao": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "keycloak": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "linkedin_oidc": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "notion": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "slack_oidc": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "spotify": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "twitch": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "twitter": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "workos": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "x": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "zoom": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + } + }, + "hook": { + "before_user_created": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "custom_access_token": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "mfa_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "password_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_email": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_sms": { + "enabled": false, + "secrets": "", + "uri": "" + } + }, + "jwt_expiry": 3600, + "mfa": { + "max_enrolled_factors": 10, + "phone": { + "enroll_enabled": false, + "max_frequency": "5s", + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "verify_enabled": false + }, + "totp": { + "enroll_enabled": false, + "verify_enabled": false + }, + "web_authn": { + "enroll_enabled": false, + "verify_enabled": false + } + }, + "minimum_password_length": 6, + "oauth_server": { + "allow_dynamic_registration": false, + "authorization_url_path": "/oauth/consent", + "enabled": false + }, + "password_requirements": "", + "rate_limit": { + "anonymous_users": 30, + "email_sent": 2, + "sign_in_sign_ups": 30, + "sms_sent": 30, + "token_refresh": 150, + "token_verifications": 30, + "web3": 30 + }, + "refresh_token_reuse_interval": 10, + "sessions": {}, + "site_url": "http://base.example.com", + "sms": { + "enable_confirmations": false, + "enable_signup": false, + "max_frequency": "5s", + "messagebird": { + "enabled": false + }, + "otp_expiry": 60, + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "textlocal": { + "enabled": false + }, + "twilio": { + "account_sid": "", + "enabled": false, + "message_service_sid": "" + }, + "twilio_verify": { + "enabled": false + }, + "vonage": { + "enabled": false + } + }, + "third_party": { + "auth0": { + "enabled": false + }, + "aws_cognito": { + "enabled": false + }, + "clerk": { + "enabled": false + }, + "firebase": { + "enabled": false + }, + "workos": { + "enabled": false + } + }, + "web3": { + "ethereum": { + "enabled": false + }, + "solana": { + "enabled": false + } + } + }, + "compute": {}, + "db": { + "health_timeout": "2m", + "major_version": 17, + "migrations": { + "enabled": true, + "schema_paths": [] + }, + "network_restrictions": { + "allowed_cidrs": [ + "0.0.0.0/0" + ], + "allowed_cidrs_v6": [ + "::/0" + ], + "enabled": false + }, + "pooler": { + "default_pool_size": 20, + "enabled": false, + "max_client_conn": 100, + "pool_mode": "transaction", + "port": 54329 + }, + "port": 55322, + "seed": { + "enabled": true, + "sql_paths": [ + "supabase/seed.sql" + ] + }, + "settings": {}, + "shadow_port": 54320, + "ssl_enforcement": { + "enabled": false + } + }, + "edge_runtime": { + "deno_version": 2, + "enabled": true, + "inspector_port": 8083, + "policy": "per_worker" + }, + "experimental": { + "inspect": { + "rules": [] + }, + "pgdelta": { + "enabled": true + }, + "webhooks": { + "enabled": false + } + }, + "functions": {}, + "local_smtp": { + "enabled": true, + "port": 54324 + }, + "project_id": "golden-stack", + "realtime": { + "enabled": true, + "ip_version": "IPv4", + "max_header_length": 4096 + }, + "remotes": { + "prod": { + "analytics": { + "backend": "postgres", + "enabled": true, + "port": 54327 + }, + "api": { + "enabled": true, + "extra_search_path": [ + "public", + "extensions" + ], + "max_rows": 1000, + "port": 54321, + "schemas": [ + "public", + "remote_api" + ], + "tls": { + "enabled": false + } + }, + "auth": { + "additional_redirect_urls": [ + "https://127.0.0.1:3000" + ], + "captcha": { + "enabled": false + }, + "email": { + "double_confirm_changes": true, + "enable_confirmations": false, + "enable_signup": true, + "max_frequency": "1s", + "notification": {}, + "otp_expiry": 3600, + "otp_length": 6, + "secure_password_change": false, + "smtp": { + "enabled": false + }, + "template": {} + }, + "enable_anonymous_sign_ins": false, + "enable_manual_linking": false, + "enable_refresh_token_rotation": true, + "enable_signup": false, + "enabled": true, + "external": { + "apple": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "azure": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "bitbucket": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "discord": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "facebook": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "figma": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "github": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "gitlab": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "https://gitlab.com" + }, + "google": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "kakao": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "keycloak": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "linkedin_oidc": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "notion": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "slack_oidc": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "spotify": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "twitch": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "twitter": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "workos": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "x": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "zoom": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + } + }, + "hook": { + "before_user_created": { + "enabled": false + }, + "custom_access_token": { + "enabled": false + }, + "mfa_verification_attempt": { + "enabled": false + }, + "password_verification_attempt": { + "enabled": false + }, + "send_email": { + "enabled": false + }, + "send_sms": { + "enabled": false + } + }, + "jwt_expiry": 3600, + "mfa": { + "max_enrolled_factors": 10, + "phone": { + "enroll_enabled": false, + "max_frequency": "5s", + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "verify_enabled": false + }, + "totp": { + "enroll_enabled": false, + "verify_enabled": false + }, + "web_authn": { + "enroll_enabled": false, + "verify_enabled": false + } + }, + "minimum_password_length": 6, + "oauth_server": { + "allow_dynamic_registration": false, + "authorization_url_path": "/oauth/consent", + "enabled": false + }, + "password_requirements": "", + "rate_limit": { + "anonymous_users": 30, + "email_sent": 2, + "sign_in_sign_ups": 30, + "sms_sent": 30, + "token_refresh": 150, + "token_verifications": 30, + "web3": 30 + }, + "refresh_token_reuse_interval": 10, + "sessions": {}, + "site_url": "http://remote.example.com", + "sms": { + "enable_confirmations": false, + "enable_signup": false, + "max_frequency": "5s", + "messagebird": { + "enabled": false + }, + "otp_expiry": 60, + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "textlocal": { + "enabled": false + }, + "twilio": { + "account_sid": "", + "enabled": false, + "message_service_sid": "" + }, + "twilio_verify": { + "enabled": false + }, + "vonage": { + "enabled": false + } + }, + "third_party": { + "auth0": { + "enabled": false + }, + "aws_cognito": { + "enabled": false + }, + "clerk": { + "enabled": false + }, + "firebase": { + "enabled": false + }, + "workos": { + "enabled": false + } + }, + "web3": { + "ethereum": { + "enabled": false + }, + "solana": { + "enabled": false + } + } + }, + "compute": {}, + "db": { + "health_timeout": "2m", + "major_version": 17, + "migrations": { + "enabled": true, + "schema_paths": [] + }, + "network_restrictions": { + "allowed_cidrs": [ + "0.0.0.0/0" + ], + "allowed_cidrs_v6": [ + "::/0" + ], + "enabled": false + }, + "pooler": { + "default_pool_size": 20, + "enabled": false, + "max_client_conn": 100, + "pool_mode": "transaction", + "port": 54329 + }, + "port": 54322, + "seed": { + "enabled": true, + "sql_paths": [ + "./seed.sql" + ] + }, + "settings": {}, + "shadow_port": 54320, + "ssl_enforcement": { + "enabled": false + } + }, + "edge_runtime": { + "deno_version": 2, + "enabled": true, + "inspector_port": 8083, + "policy": "per_worker" + }, + "experimental": { + "inspect": { + "rules": [] + }, + "pgdelta": { + "enabled": true + }, + "webhooks": { + "enabled": false + } + }, + "functions": {}, + "local_smtp": { + "enabled": true, + "port": 54324 + }, + "project_id": "abcdefghijklmnopqrst", + "realtime": { + "enabled": false, + "ip_version": "IPv4", + "max_header_length": 4096 + }, + "storage": { + "analytics": { + "buckets": {}, + "enabled": false, + "max_catalogs": 2, + "max_namespaces": 5, + "max_tables": 10 + }, + "enabled": true, + "file_size_limit": "50MiB", + "image_transformation": { + "enabled": false + }, + "s3_protocol": { + "enabled": true + }, + "vector": { + "buckets": {}, + "enabled": true, + "max_buckets": 10, + "max_indexes": 5 + } + }, + "studio": { + "api_url": "http://127.0.0.1", + "enabled": true, + "port": 54323 + } + } + }, + "storage": { + "analytics": { + "buckets": {}, + "enabled": false, + "max_catalogs": 2, + "max_namespaces": 5, + "max_tables": 10 + }, + "enabled": true, + "file_size_limit": "50MiB", + "image_transformation": { + "enabled": false + }, + "s3_protocol": { + "enabled": true + }, + "vector": { + "buckets": {}, + "enabled": true, + "max_buckets": 10, + "max_indexes": 5 + } + }, + "studio": { + "api_url": "http://127.0.0.1", + "enabled": true, + "port": 55323 + } + } +} diff --git a/apps/cli/src/command-internal/testdata/config-precedence/stack-config/remote-block-merged-via-context-with-shell-env.json b/apps/cli/src/command-internal/testdata/config-precedence/stack-config/remote-block-merged-via-context-with-shell-env.json new file mode 100644 index 0000000000..4b6d8a4fcf --- /dev/null +++ b/apps/cli/src/command-internal/testdata/config-precedence/stack-config/remote-block-merged-via-context-with-shell-env.json @@ -0,0 +1,672 @@ +{ + "creations": [ + { + "config": { + "healthTimeoutMs": 120000, + "jwtExpiry": 3600, + "settings": {} + }, + "endpoints": { + "sql": { + "port": 56322 + } + }, + "service": "database" + }, + { + "config": { + "apiKey": "api-key", + "backend": "postgres" + }, + "endpoints": { + "http": { + "port": "auto" + } + }, + "service": "analytics" + }, + { + "config": { + "extraSearchPath": "public,extensions", + "maxRows": 1000, + "schemas": "public,shell_api" + }, + "endpoints": { + "http": { + "port": 56321 + } + }, + "service": "rest" + }, + { + "config": { + "disableSignup": true, + "jwtExpiry": 3600, + "settings": { + "additionalRedirectUrls": "https://127.0.0.1:3000", + "captcha": { + "enabled": false + }, + "email": { + "double_confirm_changes": true, + "enable_confirmations": false, + "enable_signup": true, + "max_frequency": "1s", + "notification": {}, + "otp_expiry": 3600, + "otp_length": 6, + "secure_password_change": false, + "template": {} + }, + "enableAnonymousSignIns": false, + "enableManualLinking": false, + "enableRefreshTokenRotation": true, + "external": { + "apple": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + } + }, + "hooks": { + "before_user_created": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "custom_access_token": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "mfa_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "password_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_email": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_sms": { + "enabled": false, + "secrets": "", + "uri": "" + } + }, + "mfa": { + "max_enrolled_factors": 10, + "phone": { + "enroll_enabled": false, + "max_frequency": "5s", + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "verify_enabled": false + }, + "totp": { + "enroll_enabled": false, + "verify_enabled": false + }, + "web_authn": { + "enroll_enabled": false, + "verify_enabled": false + } + }, + "minimumPasswordLength": 6, + "oauthServer": { + "allow_dynamic_registration": false, + "authorization_url_path": "/oauth/consent", + "enabled": false + }, + "passwordRequirements": "", + "rateLimit": { + "anonymous_users": 30, + "email_sent": 2, + "sign_in_sign_ups": 30, + "sms_sent": 30, + "token_refresh": 150, + "token_verifications": 30, + "web3": 30 + }, + "refreshTokenReuseInterval": 10, + "sessions": {}, + "sms": { + "enable_confirmations": false, + "enable_signup": false, + "max_frequency": "5s", + "messagebird": { + "enabled": false + }, + "otp_expiry": 60, + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "textlocal": { + "enabled": false + }, + "twilio": { + "account_sid": "", + "enabled": false, + "message_service_sid": "" + }, + "twilio_verify": { + "enabled": false + }, + "vonage": { + "enabled": false + } + }, + "web3": { + "ethereum": { + "enabled": false + }, + "solana": { + "enabled": false + } + } + }, + "siteUrl": "http://shell.example.com", + "smtpAdminEmail": "admin@email.com", + "smtpSenderName": "Admin" + }, + "endpoints": { + "http": { + "port": 56321 + } + }, + "service": "auth" + }, + { + "config": { + "filePath": "/stacks/golden-stack/uploads", + "fileSizeLimit": "52428800", + "s3AccessKeyId": "625729a08b95bf1b7ff351a663f3a23c", + "s3ProtocolEnabled": true, + "s3Region": "local", + "s3SecretAccessKey": "850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907", + "vectorEnabled": true, + "vectorMaxBuckets": 10, + "vectorMaxIndexes": 5 + }, + "endpoints": { + "http": { + "port": 56321 + } + }, + "service": "storage" + }, + { + "config": { + "env": {}, + "filesRoot": "", + "functions": {}, + "functionsRoot": "/supabase/functions", + "policy": "per_worker", + "verifyJwt": true + }, + "endpoints": { + "http": { + "port": 56321 + }, + "inspector": { + "port": "auto" + } + }, + "service": "functions" + }, + { + "config": {}, + "endpoints": { + "http": { + "port": "auto" + }, + "pop3": { + "port": "auto" + }, + "smtp": { + "port": "auto" + } + }, + "service": "mail" + } + ], + "keys": { + "anonKeyIsOverride": false, + "serviceRoleKeyIsOverride": false + }, + "projectEnvValues": {}, + "source": { + "analytics": { + "backend": "postgres", + "enabled": true, + "port": 54327 + }, + "api": { + "enabled": true, + "extra_search_path": [ + "public", + "extensions" + ], + "max_rows": 1000, + "port": 56321, + "schemas": [ + "public", + "shell_api" + ], + "tls": { + "enabled": false + } + }, + "auth": { + "additional_redirect_urls": [ + "https://127.0.0.1:3000" + ], + "captcha": { + "enabled": false + }, + "email": { + "double_confirm_changes": true, + "enable_confirmations": false, + "enable_signup": true, + "max_frequency": "1s", + "notification": {}, + "otp_expiry": 3600, + "otp_length": 6, + "secure_password_change": false, + "smtp": { + "enabled": false + }, + "template": {} + }, + "enable_anonymous_sign_ins": false, + "enable_manual_linking": false, + "enable_refresh_token_rotation": true, + "enable_signup": false, + "enabled": true, + "external": { + "apple": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "azure": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "bitbucket": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "discord": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "facebook": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "figma": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "github": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "gitlab": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "https://gitlab.com" + }, + "google": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "kakao": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "keycloak": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "linkedin_oidc": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "notion": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "slack_oidc": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "spotify": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "twitch": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "twitter": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "workos": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "x": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "zoom": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + } + }, + "hook": { + "before_user_created": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "custom_access_token": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "mfa_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "password_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_email": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_sms": { + "enabled": false, + "secrets": "", + "uri": "" + } + }, + "jwt_expiry": 3600, + "mfa": { + "max_enrolled_factors": 10, + "phone": { + "enroll_enabled": false, + "max_frequency": "5s", + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "verify_enabled": false + }, + "totp": { + "enroll_enabled": false, + "verify_enabled": false + }, + "web_authn": { + "enroll_enabled": false, + "verify_enabled": false + } + }, + "minimum_password_length": 6, + "oauth_server": { + "allow_dynamic_registration": false, + "authorization_url_path": "/oauth/consent", + "enabled": false + }, + "password_requirements": "", + "rate_limit": { + "anonymous_users": 30, + "email_sent": 2, + "sign_in_sign_ups": 30, + "sms_sent": 30, + "token_refresh": 150, + "token_verifications": 30, + "web3": 30 + }, + "refresh_token_reuse_interval": 10, + "sessions": {}, + "site_url": "http://shell.example.com", + "sms": { + "enable_confirmations": false, + "enable_signup": false, + "max_frequency": "5s", + "messagebird": { + "enabled": false + }, + "otp_expiry": 60, + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "textlocal": { + "enabled": false + }, + "twilio": { + "account_sid": "", + "enabled": false, + "message_service_sid": "" + }, + "twilio_verify": { + "enabled": false + }, + "vonage": { + "enabled": false + } + }, + "third_party": { + "auth0": { + "enabled": false + }, + "aws_cognito": { + "enabled": false + }, + "clerk": { + "enabled": false + }, + "firebase": { + "enabled": false + }, + "workos": { + "enabled": false + } + }, + "web3": { + "ethereum": { + "enabled": false + }, + "solana": { + "enabled": false + } + } + }, + "compute": {}, + "db": { + "health_timeout": "2m", + "major_version": 17, + "migrations": { + "enabled": true, + "schema_paths": [] + }, + "network_restrictions": { + "allowed_cidrs": [ + "0.0.0.0/0" + ], + "allowed_cidrs_v6": [ + "::/0" + ], + "enabled": false + }, + "pooler": { + "default_pool_size": 20, + "enabled": false, + "max_client_conn": 100, + "pool_mode": "transaction", + "port": 54329 + }, + "port": 56322, + "seed": { + "enabled": false, + "sql_paths": [ + "supabase/seed.sql" + ] + }, + "settings": {}, + "shadow_port": 54320, + "ssl_enforcement": { + "enabled": false + } + }, + "edge_runtime": { + "deno_version": 2, + "enabled": true, + "inspector_port": 8083, + "policy": "per_worker" + }, + "experimental": { + "inspect": { + "rules": [] + }, + "pgdelta": { + "enabled": true + }, + "webhooks": { + "enabled": false + } + }, + "functions": {}, + "local_smtp": { + "enabled": true, + "port": 54324 + }, + "project_id": "abcdefghijklmnopqrst", + "realtime": { + "enabled": false, + "ip_version": "IPv4", + "max_header_length": 4096 + }, + "remotes": {}, + "storage": { + "analytics": { + "buckets": {}, + "enabled": false, + "max_catalogs": 2, + "max_namespaces": 5, + "max_tables": 10 + }, + "enabled": true, + "file_size_limit": "50MiB", + "image_transformation": { + "enabled": false + }, + "s3_protocol": { + "enabled": true + }, + "vector": { + "buckets": {}, + "enabled": true, + "max_buckets": 10, + "max_indexes": 5 + } + }, + "studio": { + "api_url": "http://127.0.0.1", + "enabled": false, + "port": 55323 + } + } +} diff --git a/apps/cli/src/command-internal/testdata/config-precedence/stack-config/remote-block-merged-via-context.json b/apps/cli/src/command-internal/testdata/config-precedence/stack-config/remote-block-merged-via-context.json new file mode 100644 index 0000000000..da40f6b4d6 --- /dev/null +++ b/apps/cli/src/command-internal/testdata/config-precedence/stack-config/remote-block-merged-via-context.json @@ -0,0 +1,696 @@ +{ + "creations": [ + { + "config": { + "healthTimeoutMs": 120000, + "jwtExpiry": 3600, + "settings": {} + }, + "endpoints": { + "sql": { + "port": 55322 + } + }, + "service": "database" + }, + { + "config": { + "apiKey": "api-key", + "backend": "postgres" + }, + "endpoints": { + "http": { + "port": "auto" + } + }, + "service": "analytics" + }, + { + "config": {}, + "endpoints": { + "http": { + "port": "auto" + } + }, + "service": "pgmeta" + }, + { + "config": { + "extraSearchPath": "public,extensions", + "maxRows": 1000, + "schemas": "public,remote_api" + }, + "endpoints": { + "http": { + "port": 55321 + } + }, + "service": "rest" + }, + { + "config": { + "disableSignup": true, + "jwtExpiry": 3600, + "settings": { + "additionalRedirectUrls": "https://127.0.0.1:3000", + "captcha": { + "enabled": false + }, + "email": { + "double_confirm_changes": true, + "enable_confirmations": false, + "enable_signup": true, + "max_frequency": "1s", + "notification": {}, + "otp_expiry": 3600, + "otp_length": 6, + "secure_password_change": false, + "template": {} + }, + "enableAnonymousSignIns": false, + "enableManualLinking": false, + "enableRefreshTokenRotation": true, + "external": { + "apple": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + } + }, + "hooks": { + "before_user_created": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "custom_access_token": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "mfa_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "password_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_email": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_sms": { + "enabled": false, + "secrets": "", + "uri": "" + } + }, + "mfa": { + "max_enrolled_factors": 10, + "phone": { + "enroll_enabled": false, + "max_frequency": "5s", + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "verify_enabled": false + }, + "totp": { + "enroll_enabled": false, + "verify_enabled": false + }, + "web_authn": { + "enroll_enabled": false, + "verify_enabled": false + } + }, + "minimumPasswordLength": 6, + "oauthServer": { + "allow_dynamic_registration": false, + "authorization_url_path": "/oauth/consent", + "enabled": false + }, + "passwordRequirements": "", + "rateLimit": { + "anonymous_users": 30, + "email_sent": 2, + "sign_in_sign_ups": 30, + "sms_sent": 30, + "token_refresh": 150, + "token_verifications": 30, + "web3": 30 + }, + "refreshTokenReuseInterval": 10, + "sessions": {}, + "sms": { + "enable_confirmations": false, + "enable_signup": false, + "max_frequency": "5s", + "messagebird": { + "enabled": false + }, + "otp_expiry": 60, + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "textlocal": { + "enabled": false + }, + "twilio": { + "account_sid": "", + "enabled": false, + "message_service_sid": "" + }, + "twilio_verify": { + "enabled": false + }, + "vonage": { + "enabled": false + } + }, + "web3": { + "ethereum": { + "enabled": false + }, + "solana": { + "enabled": false + } + } + }, + "siteUrl": "http://remote.example.com", + "smtpAdminEmail": "admin@email.com", + "smtpSenderName": "Admin" + }, + "endpoints": { + "http": { + "port": 55321 + } + }, + "service": "auth" + }, + { + "config": { + "filePath": "/stacks/golden-stack/uploads", + "fileSizeLimit": "52428800", + "s3AccessKeyId": "625729a08b95bf1b7ff351a663f3a23c", + "s3ProtocolEnabled": true, + "s3Region": "local", + "s3SecretAccessKey": "850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907", + "vectorEnabled": true, + "vectorMaxBuckets": 10, + "vectorMaxIndexes": 5 + }, + "endpoints": { + "http": { + "port": 55321 + } + }, + "service": "storage" + }, + { + "config": { + "env": {}, + "filesRoot": "", + "functions": {}, + "functionsRoot": "/supabase/functions", + "policy": "per_worker", + "verifyJwt": true + }, + "endpoints": { + "http": { + "port": 55321 + }, + "inspector": { + "port": "auto" + } + }, + "service": "functions" + }, + { + "config": { + "apiExtraSearchPath": "public,extensions", + "apiMaxRows": 1000, + "apiSchemas": "public,remote_api", + "cliVersion": "0.0.0-dev", + "snippetsRoot": "/supabase/snippets" + }, + "endpoints": { + "http": { + "port": 55323 + } + }, + "service": "studio" + }, + { + "config": {}, + "endpoints": { + "http": { + "port": "auto" + }, + "pop3": { + "port": "auto" + }, + "smtp": { + "port": "auto" + } + }, + "service": "mail" + } + ], + "keys": { + "anonKeyIsOverride": false, + "serviceRoleKeyIsOverride": false + }, + "projectEnvValues": {}, + "source": { + "analytics": { + "backend": "postgres", + "enabled": true, + "port": 54327 + }, + "api": { + "enabled": true, + "extra_search_path": [ + "public", + "extensions" + ], + "max_rows": 1000, + "port": 55321, + "schemas": [ + "public", + "remote_api" + ], + "tls": { + "enabled": false + } + }, + "auth": { + "additional_redirect_urls": [ + "https://127.0.0.1:3000" + ], + "captcha": { + "enabled": false + }, + "email": { + "double_confirm_changes": true, + "enable_confirmations": false, + "enable_signup": true, + "max_frequency": "1s", + "notification": {}, + "otp_expiry": 3600, + "otp_length": 6, + "secure_password_change": false, + "smtp": { + "enabled": false + }, + "template": {} + }, + "enable_anonymous_sign_ins": false, + "enable_manual_linking": false, + "enable_refresh_token_rotation": true, + "enable_signup": false, + "enabled": true, + "external": { + "apple": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "azure": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "bitbucket": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "discord": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "facebook": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "figma": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "github": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "gitlab": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "https://gitlab.com" + }, + "google": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "kakao": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "keycloak": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "linkedin_oidc": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "notion": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "slack_oidc": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "spotify": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "twitch": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "twitter": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "workos": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "x": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "zoom": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + } + }, + "hook": { + "before_user_created": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "custom_access_token": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "mfa_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "password_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_email": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_sms": { + "enabled": false, + "secrets": "", + "uri": "" + } + }, + "jwt_expiry": 3600, + "mfa": { + "max_enrolled_factors": 10, + "phone": { + "enroll_enabled": false, + "max_frequency": "5s", + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "verify_enabled": false + }, + "totp": { + "enroll_enabled": false, + "verify_enabled": false + }, + "web_authn": { + "enroll_enabled": false, + "verify_enabled": false + } + }, + "minimum_password_length": 6, + "oauth_server": { + "allow_dynamic_registration": false, + "authorization_url_path": "/oauth/consent", + "enabled": false + }, + "password_requirements": "", + "rate_limit": { + "anonymous_users": 30, + "email_sent": 2, + "sign_in_sign_ups": 30, + "sms_sent": 30, + "token_refresh": 150, + "token_verifications": 30, + "web3": 30 + }, + "refresh_token_reuse_interval": 10, + "sessions": {}, + "site_url": "http://remote.example.com", + "sms": { + "enable_confirmations": false, + "enable_signup": false, + "max_frequency": "5s", + "messagebird": { + "enabled": false + }, + "otp_expiry": 60, + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "textlocal": { + "enabled": false + }, + "twilio": { + "account_sid": "", + "enabled": false, + "message_service_sid": "" + }, + "twilio_verify": { + "enabled": false + }, + "vonage": { + "enabled": false + } + }, + "third_party": { + "auth0": { + "enabled": false + }, + "aws_cognito": { + "enabled": false + }, + "clerk": { + "enabled": false + }, + "firebase": { + "enabled": false + }, + "workos": { + "enabled": false + } + }, + "web3": { + "ethereum": { + "enabled": false + }, + "solana": { + "enabled": false + } + } + }, + "compute": {}, + "db": { + "health_timeout": "2m", + "major_version": 17, + "migrations": { + "enabled": true, + "schema_paths": [] + }, + "network_restrictions": { + "allowed_cidrs": [ + "0.0.0.0/0" + ], + "allowed_cidrs_v6": [ + "::/0" + ], + "enabled": false + }, + "pooler": { + "default_pool_size": 20, + "enabled": false, + "max_client_conn": 100, + "pool_mode": "transaction", + "port": 54329 + }, + "port": 55322, + "seed": { + "enabled": false, + "sql_paths": [ + "supabase/seed.sql" + ] + }, + "settings": {}, + "shadow_port": 54320, + "ssl_enforcement": { + "enabled": false + } + }, + "edge_runtime": { + "deno_version": 2, + "enabled": true, + "inspector_port": 8083, + "policy": "per_worker" + }, + "experimental": { + "inspect": { + "rules": [] + }, + "pgdelta": { + "enabled": true + }, + "webhooks": { + "enabled": false + } + }, + "functions": {}, + "local_smtp": { + "enabled": true, + "port": 54324 + }, + "project_id": "abcdefghijklmnopqrst", + "realtime": { + "enabled": false, + "ip_version": "IPv4", + "max_header_length": 4096 + }, + "remotes": {}, + "storage": { + "analytics": { + "buckets": {}, + "enabled": false, + "max_catalogs": 2, + "max_namespaces": 5, + "max_tables": 10 + }, + "enabled": true, + "file_size_limit": "50MiB", + "image_transformation": { + "enabled": false + }, + "s3_protocol": { + "enabled": true + }, + "vector": { + "buckets": {}, + "enabled": true, + "max_buckets": 10, + "max_indexes": 5 + } + }, + "studio": { + "api_url": "http://127.0.0.1", + "enabled": true, + "port": 55323 + } + } +} diff --git a/apps/cli/src/command-internal/testdata/config-precedence/stack-config/shell-beats-dotenv.json b/apps/cli/src/command-internal/testdata/config-precedence/stack-config/shell-beats-dotenv.json new file mode 100644 index 0000000000..e8476e18d0 --- /dev/null +++ b/apps/cli/src/command-internal/testdata/config-precedence/stack-config/shell-beats-dotenv.json @@ -0,0 +1,678 @@ +{ + "creations": [ + { + "config": { + "healthTimeoutMs": 120000, + "jwtExpiry": 3600, + "settings": {} + }, + "endpoints": { + "sql": { + "port": 57322 + } + }, + "service": "database" + }, + { + "config": { + "apiKey": "api-key", + "backend": "postgres" + }, + "endpoints": { + "http": { + "port": "auto" + } + }, + "service": "analytics" + }, + { + "config": { + "extraSearchPath": "public,extensions", + "maxRows": 1000, + "schemas": "public,shell_api" + }, + "endpoints": { + "http": { + "port": 57321 + } + }, + "service": "rest" + }, + { + "config": { + "disableSignup": true, + "jwtExpiry": 3600, + "settings": { + "additionalRedirectUrls": "https://127.0.0.1:3000", + "captcha": { + "enabled": false + }, + "email": { + "double_confirm_changes": true, + "enable_confirmations": false, + "enable_signup": true, + "max_frequency": "1s", + "notification": {}, + "otp_expiry": 3600, + "otp_length": 6, + "secure_password_change": false, + "template": {} + }, + "enableAnonymousSignIns": false, + "enableManualLinking": false, + "enableRefreshTokenRotation": true, + "external": { + "apple": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + } + }, + "hooks": { + "before_user_created": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "custom_access_token": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "mfa_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "password_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_email": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_sms": { + "enabled": false, + "secrets": "", + "uri": "" + } + }, + "mfa": { + "max_enrolled_factors": 10, + "phone": { + "enroll_enabled": false, + "max_frequency": "5s", + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "verify_enabled": false + }, + "totp": { + "enroll_enabled": false, + "verify_enabled": false + }, + "web_authn": { + "enroll_enabled": false, + "verify_enabled": false + } + }, + "minimumPasswordLength": 6, + "oauthServer": { + "allow_dynamic_registration": false, + "authorization_url_path": "/oauth/consent", + "enabled": false + }, + "passwordRequirements": "", + "rateLimit": { + "anonymous_users": 30, + "email_sent": 2, + "sign_in_sign_ups": 30, + "sms_sent": 30, + "token_refresh": 150, + "token_verifications": 30, + "web3": 30 + }, + "refreshTokenReuseInterval": 10, + "sessions": {}, + "sms": { + "enable_confirmations": false, + "enable_signup": false, + "max_frequency": "5s", + "messagebird": { + "enabled": false + }, + "otp_expiry": 60, + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "textlocal": { + "enabled": false + }, + "twilio": { + "account_sid": "", + "enabled": false, + "message_service_sid": "" + }, + "twilio_verify": { + "enabled": false + }, + "vonage": { + "enabled": false + } + }, + "web3": { + "ethereum": { + "enabled": false + }, + "solana": { + "enabled": false + } + } + }, + "siteUrl": "http://shell.example.com", + "smtpAdminEmail": "admin@email.com", + "smtpSenderName": "Admin" + }, + "endpoints": { + "http": { + "port": 57321 + } + }, + "service": "auth" + }, + { + "config": { + "filePath": "/stacks/golden-stack/uploads", + "fileSizeLimit": "52428800", + "s3AccessKeyId": "625729a08b95bf1b7ff351a663f3a23c", + "s3ProtocolEnabled": true, + "s3Region": "local", + "s3SecretAccessKey": "850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907", + "vectorEnabled": true, + "vectorMaxBuckets": 10, + "vectorMaxIndexes": 5 + }, + "endpoints": { + "http": { + "port": 57321 + } + }, + "service": "storage" + }, + { + "config": { + "env": {}, + "filesRoot": "", + "functions": {}, + "functionsRoot": "/supabase/functions", + "policy": "per_worker", + "verifyJwt": true + }, + "endpoints": { + "http": { + "port": 57321 + }, + "inspector": { + "port": "auto" + } + }, + "service": "functions" + }, + { + "config": {}, + "endpoints": { + "http": { + "port": "auto" + }, + "pop3": { + "port": "auto" + }, + "smtp": { + "port": "auto" + } + }, + "service": "mail" + } + ], + "keys": { + "anonKeyIsOverride": false, + "serviceRoleKeyIsOverride": false + }, + "projectEnvValues": { + "SUPABASE_API_PORT": "57321", + "SUPABASE_AUTH_ENABLE_SIGNUP": "false", + "SUPABASE_DB_PORT": "57322", + "SUPABASE_REALTIME_ENABLED": "false", + "SUPABASE_STUDIO_ENABLED": "false" + }, + "source": { + "analytics": { + "backend": "postgres", + "enabled": true, + "port": 54327 + }, + "api": { + "enabled": true, + "extra_search_path": [ + "public", + "extensions" + ], + "max_rows": 1000, + "port": 57321, + "schemas": [ + "public", + "shell_api" + ], + "tls": { + "enabled": false + } + }, + "auth": { + "additional_redirect_urls": [ + "https://127.0.0.1:3000" + ], + "captcha": { + "enabled": false + }, + "email": { + "double_confirm_changes": true, + "enable_confirmations": false, + "enable_signup": true, + "max_frequency": "1s", + "notification": {}, + "otp_expiry": 3600, + "otp_length": 6, + "secure_password_change": false, + "smtp": { + "enabled": false + }, + "template": {} + }, + "enable_anonymous_sign_ins": false, + "enable_manual_linking": false, + "enable_refresh_token_rotation": true, + "enable_signup": false, + "enabled": true, + "external": { + "apple": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "azure": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "bitbucket": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "discord": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "facebook": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "figma": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "github": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "gitlab": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "https://gitlab.com" + }, + "google": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "kakao": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "keycloak": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "linkedin_oidc": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "notion": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "slack_oidc": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "spotify": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "twitch": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "twitter": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "workos": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "x": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "zoom": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + } + }, + "hook": { + "before_user_created": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "custom_access_token": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "mfa_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "password_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_email": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_sms": { + "enabled": false, + "secrets": "", + "uri": "" + } + }, + "jwt_expiry": 3600, + "mfa": { + "max_enrolled_factors": 10, + "phone": { + "enroll_enabled": false, + "max_frequency": "5s", + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "verify_enabled": false + }, + "totp": { + "enroll_enabled": false, + "verify_enabled": false + }, + "web_authn": { + "enroll_enabled": false, + "verify_enabled": false + } + }, + "minimum_password_length": 6, + "oauth_server": { + "allow_dynamic_registration": false, + "authorization_url_path": "/oauth/consent", + "enabled": false + }, + "password_requirements": "", + "rate_limit": { + "anonymous_users": 30, + "email_sent": 2, + "sign_in_sign_ups": 30, + "sms_sent": 30, + "token_refresh": 150, + "token_verifications": 30, + "web3": 30 + }, + "refresh_token_reuse_interval": 10, + "sessions": {}, + "site_url": "http://shell.example.com", + "sms": { + "enable_confirmations": false, + "enable_signup": false, + "max_frequency": "5s", + "messagebird": { + "enabled": false + }, + "otp_expiry": 60, + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "textlocal": { + "enabled": false + }, + "twilio": { + "account_sid": "", + "enabled": false, + "message_service_sid": "" + }, + "twilio_verify": { + "enabled": false + }, + "vonage": { + "enabled": false + } + }, + "third_party": { + "auth0": { + "enabled": false + }, + "aws_cognito": { + "enabled": false + }, + "clerk": { + "enabled": false + }, + "firebase": { + "enabled": false + }, + "workos": { + "enabled": false + } + }, + "web3": { + "ethereum": { + "enabled": false + }, + "solana": { + "enabled": false + } + } + }, + "compute": {}, + "db": { + "health_timeout": "2m", + "major_version": 17, + "migrations": { + "enabled": true, + "schema_paths": [] + }, + "network_restrictions": { + "allowed_cidrs": [ + "0.0.0.0/0" + ], + "allowed_cidrs_v6": [ + "::/0" + ], + "enabled": false + }, + "pooler": { + "default_pool_size": 20, + "enabled": false, + "max_client_conn": 100, + "pool_mode": "transaction", + "port": 54329 + }, + "port": 57322, + "seed": { + "enabled": true, + "sql_paths": [ + "supabase/seed.sql" + ] + }, + "settings": {}, + "shadow_port": 54320, + "ssl_enforcement": { + "enabled": false + } + }, + "edge_runtime": { + "deno_version": 2, + "enabled": true, + "inspector_port": 8083, + "policy": "per_worker" + }, + "experimental": { + "inspect": { + "rules": [] + }, + "pgdelta": { + "enabled": true + }, + "webhooks": { + "enabled": false + } + }, + "functions": {}, + "local_smtp": { + "enabled": true, + "port": 54324 + }, + "project_id": "golden-stack", + "realtime": { + "enabled": false, + "ip_version": "IPv4", + "max_header_length": 4096 + }, + "remotes": {}, + "storage": { + "analytics": { + "buckets": {}, + "enabled": false, + "max_catalogs": 2, + "max_namespaces": 5, + "max_tables": 10 + }, + "enabled": true, + "file_size_limit": "50MiB", + "image_transformation": { + "enabled": false + }, + "s3_protocol": { + "enabled": true + }, + "vector": { + "buckets": {}, + "enabled": true, + "max_buckets": 10, + "max_indexes": 5 + } + }, + "studio": { + "api_url": "http://127.0.0.1", + "enabled": false, + "port": 55323 + } + } +} diff --git a/apps/cli/src/command-internal/testdata/config-precedence/stack-config/shell-env.json b/apps/cli/src/command-internal/testdata/config-precedence/stack-config/shell-env.json new file mode 100644 index 0000000000..41a7615602 --- /dev/null +++ b/apps/cli/src/command-internal/testdata/config-precedence/stack-config/shell-env.json @@ -0,0 +1,672 @@ +{ + "creations": [ + { + "config": { + "healthTimeoutMs": 120000, + "jwtExpiry": 3600, + "settings": {} + }, + "endpoints": { + "sql": { + "port": 56322 + } + }, + "service": "database" + }, + { + "config": { + "apiKey": "api-key", + "backend": "postgres" + }, + "endpoints": { + "http": { + "port": "auto" + } + }, + "service": "analytics" + }, + { + "config": { + "extraSearchPath": "public,extensions", + "maxRows": 1000, + "schemas": "public,shell_api" + }, + "endpoints": { + "http": { + "port": 56321 + } + }, + "service": "rest" + }, + { + "config": { + "disableSignup": true, + "jwtExpiry": 3600, + "settings": { + "additionalRedirectUrls": "https://127.0.0.1:3000", + "captcha": { + "enabled": false + }, + "email": { + "double_confirm_changes": true, + "enable_confirmations": false, + "enable_signup": true, + "max_frequency": "1s", + "notification": {}, + "otp_expiry": 3600, + "otp_length": 6, + "secure_password_change": false, + "template": {} + }, + "enableAnonymousSignIns": false, + "enableManualLinking": false, + "enableRefreshTokenRotation": true, + "external": { + "apple": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + } + }, + "hooks": { + "before_user_created": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "custom_access_token": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "mfa_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "password_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_email": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_sms": { + "enabled": false, + "secrets": "", + "uri": "" + } + }, + "mfa": { + "max_enrolled_factors": 10, + "phone": { + "enroll_enabled": false, + "max_frequency": "5s", + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "verify_enabled": false + }, + "totp": { + "enroll_enabled": false, + "verify_enabled": false + }, + "web_authn": { + "enroll_enabled": false, + "verify_enabled": false + } + }, + "minimumPasswordLength": 6, + "oauthServer": { + "allow_dynamic_registration": false, + "authorization_url_path": "/oauth/consent", + "enabled": false + }, + "passwordRequirements": "", + "rateLimit": { + "anonymous_users": 30, + "email_sent": 2, + "sign_in_sign_ups": 30, + "sms_sent": 30, + "token_refresh": 150, + "token_verifications": 30, + "web3": 30 + }, + "refreshTokenReuseInterval": 10, + "sessions": {}, + "sms": { + "enable_confirmations": false, + "enable_signup": false, + "max_frequency": "5s", + "messagebird": { + "enabled": false + }, + "otp_expiry": 60, + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "textlocal": { + "enabled": false + }, + "twilio": { + "account_sid": "", + "enabled": false, + "message_service_sid": "" + }, + "twilio_verify": { + "enabled": false + }, + "vonage": { + "enabled": false + } + }, + "web3": { + "ethereum": { + "enabled": false + }, + "solana": { + "enabled": false + } + } + }, + "siteUrl": "http://shell.example.com", + "smtpAdminEmail": "admin@email.com", + "smtpSenderName": "Admin" + }, + "endpoints": { + "http": { + "port": 56321 + } + }, + "service": "auth" + }, + { + "config": { + "filePath": "/stacks/golden-stack/uploads", + "fileSizeLimit": "52428800", + "s3AccessKeyId": "625729a08b95bf1b7ff351a663f3a23c", + "s3ProtocolEnabled": true, + "s3Region": "local", + "s3SecretAccessKey": "850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907", + "vectorEnabled": true, + "vectorMaxBuckets": 10, + "vectorMaxIndexes": 5 + }, + "endpoints": { + "http": { + "port": 56321 + } + }, + "service": "storage" + }, + { + "config": { + "env": {}, + "filesRoot": "", + "functions": {}, + "functionsRoot": "/supabase/functions", + "policy": "per_worker", + "verifyJwt": true + }, + "endpoints": { + "http": { + "port": 56321 + }, + "inspector": { + "port": "auto" + } + }, + "service": "functions" + }, + { + "config": {}, + "endpoints": { + "http": { + "port": "auto" + }, + "pop3": { + "port": "auto" + }, + "smtp": { + "port": "auto" + } + }, + "service": "mail" + } + ], + "keys": { + "anonKeyIsOverride": false, + "serviceRoleKeyIsOverride": false + }, + "projectEnvValues": {}, + "source": { + "analytics": { + "backend": "postgres", + "enabled": true, + "port": 54327 + }, + "api": { + "enabled": true, + "extra_search_path": [ + "public", + "extensions" + ], + "max_rows": 1000, + "port": 56321, + "schemas": [ + "public", + "shell_api" + ], + "tls": { + "enabled": false + } + }, + "auth": { + "additional_redirect_urls": [ + "https://127.0.0.1:3000" + ], + "captcha": { + "enabled": false + }, + "email": { + "double_confirm_changes": true, + "enable_confirmations": false, + "enable_signup": true, + "max_frequency": "1s", + "notification": {}, + "otp_expiry": 3600, + "otp_length": 6, + "secure_password_change": false, + "smtp": { + "enabled": false + }, + "template": {} + }, + "enable_anonymous_sign_ins": false, + "enable_manual_linking": false, + "enable_refresh_token_rotation": true, + "enable_signup": false, + "enabled": true, + "external": { + "apple": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "azure": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "bitbucket": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "discord": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "facebook": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "figma": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "github": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "gitlab": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "https://gitlab.com" + }, + "google": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "kakao": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "keycloak": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "linkedin_oidc": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "notion": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "slack_oidc": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "spotify": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "twitch": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "twitter": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "workos": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "x": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + }, + "zoom": { + "client_id": "", + "email_optional": false, + "enabled": false, + "redirect_uri": "", + "skip_nonce_check": false, + "url": "" + } + }, + "hook": { + "before_user_created": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "custom_access_token": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "mfa_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "password_verification_attempt": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_email": { + "enabled": false, + "secrets": "", + "uri": "" + }, + "send_sms": { + "enabled": false, + "secrets": "", + "uri": "" + } + }, + "jwt_expiry": 3600, + "mfa": { + "max_enrolled_factors": 10, + "phone": { + "enroll_enabled": false, + "max_frequency": "5s", + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "verify_enabled": false + }, + "totp": { + "enroll_enabled": false, + "verify_enabled": false + }, + "web_authn": { + "enroll_enabled": false, + "verify_enabled": false + } + }, + "minimum_password_length": 6, + "oauth_server": { + "allow_dynamic_registration": false, + "authorization_url_path": "/oauth/consent", + "enabled": false + }, + "password_requirements": "", + "rate_limit": { + "anonymous_users": 30, + "email_sent": 2, + "sign_in_sign_ups": 30, + "sms_sent": 30, + "token_refresh": 150, + "token_verifications": 30, + "web3": 30 + }, + "refresh_token_reuse_interval": 10, + "sessions": {}, + "site_url": "http://shell.example.com", + "sms": { + "enable_confirmations": false, + "enable_signup": false, + "max_frequency": "5s", + "messagebird": { + "enabled": false + }, + "otp_expiry": 60, + "otp_length": 6, + "template": "Your code is {{ .Code }}", + "textlocal": { + "enabled": false + }, + "twilio": { + "account_sid": "", + "enabled": false, + "message_service_sid": "" + }, + "twilio_verify": { + "enabled": false + }, + "vonage": { + "enabled": false + } + }, + "third_party": { + "auth0": { + "enabled": false + }, + "aws_cognito": { + "enabled": false + }, + "clerk": { + "enabled": false + }, + "firebase": { + "enabled": false + }, + "workos": { + "enabled": false + } + }, + "web3": { + "ethereum": { + "enabled": false + }, + "solana": { + "enabled": false + } + } + }, + "compute": {}, + "db": { + "health_timeout": "2m", + "major_version": 17, + "migrations": { + "enabled": true, + "schema_paths": [] + }, + "network_restrictions": { + "allowed_cidrs": [ + "0.0.0.0/0" + ], + "allowed_cidrs_v6": [ + "::/0" + ], + "enabled": false + }, + "pooler": { + "default_pool_size": 20, + "enabled": false, + "max_client_conn": 100, + "pool_mode": "transaction", + "port": 54329 + }, + "port": 56322, + "seed": { + "enabled": true, + "sql_paths": [ + "supabase/seed.sql" + ] + }, + "settings": {}, + "shadow_port": 54320, + "ssl_enforcement": { + "enabled": false + } + }, + "edge_runtime": { + "deno_version": 2, + "enabled": true, + "inspector_port": 8083, + "policy": "per_worker" + }, + "experimental": { + "inspect": { + "rules": [] + }, + "pgdelta": { + "enabled": true + }, + "webhooks": { + "enabled": false + } + }, + "functions": {}, + "local_smtp": { + "enabled": true, + "port": 54324 + }, + "project_id": "golden-stack", + "realtime": { + "enabled": false, + "ip_version": "IPv4", + "max_header_length": 4096 + }, + "remotes": {}, + "storage": { + "analytics": { + "buckets": {}, + "enabled": false, + "max_catalogs": 2, + "max_namespaces": 5, + "max_tables": 10 + }, + "enabled": true, + "file_size_limit": "50MiB", + "image_transformation": { + "enabled": false + }, + "s3_protocol": { + "enabled": true + }, + "vector": { + "buckets": {}, + "enabled": true, + "max_buckets": 10, + "max_indexes": 5 + } + }, + "studio": { + "api_url": "http://127.0.0.1", + "enabled": false, + "port": 55323 + } + } +} diff --git a/apps/cli/src/command-internal/timestamp.format.unit.test.ts b/apps/cli/src/command-internal/timestamp.format.unit.test.ts index c8bd2c1af8..ef79682c44 100644 --- a/apps/cli/src/command-internal/timestamp.format.unit.test.ts +++ b/apps/cli/src/command-internal/timestamp.format.unit.test.ts @@ -14,7 +14,7 @@ describe("formatTimestamp", () => { }); it("falls back to the original value for already-formatted timestamps", () => { - // Go's time.Parse(time.RFC3339, ...) rejects "2026-02-08 16:44:07" (space, not T). + // RFC3339 parsing rejects "2026-02-08 16:44:07" (space, not T). expect(formatTimestamp("2026-02-08 16:44:07")).toBe("2026-02-08 16:44:07"); }); diff --git a/apps/cli/src/command-internal/upgrade-notice.integration.test.ts b/apps/cli/src/command-internal/upgrade-notice.integration.test.ts index 34232a2d5c..417bdaad54 100644 --- a/apps/cli/src/command-internal/upgrade-notice.integration.test.ts +++ b/apps/cli/src/command-internal/upgrade-notice.integration.test.ts @@ -82,7 +82,6 @@ describe("upgrade notice user-level cache", () => { [...(options.args ?? []), "projects", "list"], { cleanShowHelp: false, - delegatedToGo: false, workingDirectory: options.workingDirectory ?? workdir, isValueTakingFlagToken: () => false, }, diff --git a/apps/cli/src/command-internal/upgrade-notice.ts b/apps/cli/src/command-internal/upgrade-notice.ts index 57c60cadfb..9ec18c5941 100644 --- a/apps/cli/src/command-internal/upgrade-notice.ts +++ b/apps/cli/src/command-internal/upgrade-notice.ts @@ -29,6 +29,7 @@ import { rootFlagTokens, } from "../shared/cli/run.ts"; import { CLI_UPGRADE_GUIDE_URL, CLI_VERSION, parseSemver } from "../shared/cli/version.ts"; +import { candidateDotenvFilenames } from "../shared/config/cli-config-env.ts"; import { resolveSupabaseHomeValue } from "../shared/config/supabase-home.ts"; import { RuntimeInfo } from "../shared/runtime/runtime-info.service.ts"; import { @@ -37,10 +38,9 @@ import { ErrorActionabilityId, } from "../shared/telemetry/error-actionability.ts"; import { bold, yellow } from "./colors.ts"; -import { legacyProjectStatePaths } from "./legacy-project-state.paths.ts"; -import { parseDotEnv } from "./dotenv.ts"; -import { candidateDotenvFilenames } from "./project-environment.ts"; -import { projectStateMode } from "./project-state-mode.ts"; +import { legacyProjectStatePaths } from "../shared/config/legacy-project-state.paths.ts"; +import { parseDotEnv } from "../shared/config/dotenv.ts"; +import { projectStateMode } from "../shared/config/project-state-mode.ts"; const LATEST_RELEASE_URL = "https://api.github.com/repos/supabase/cli/releases/latest"; const CACHE_TTL_MS = 10 * 60 * 60 * 1000; @@ -208,8 +208,10 @@ const resolveNoticeBaseDir = Effect.fnUntraced(function* ( } let current = cwd; while (true) { - const configToml = path.join(current, "supabase", "config.toml"); - if (yield* fs.exists(configToml).pipe(Effect.orElseSucceed(() => false))) return current; + for (const configFile of ["config.json", "config.toml"]) { + const configPath = path.join(current, "supabase", configFile); + if (yield* fs.exists(configPath).pipe(Effect.orElseSucceed(() => false))) return current; + } const parent = path.dirname(current); if (parent === current) return cwd; current = parent; @@ -219,7 +221,7 @@ const resolveNoticeBaseDir = Effect.fnUntraced(function* ( /** * The project dotenv chain as a merged map: `/supabase` then ``, * first file to define a key wins, shell env always beats a chain value — - * same precedence as `resolveProjectEnvironmentValues`. Read for every real + * same precedence as `loadCliProjectEnvFiles`. Read for every real * command since this hook can't tell whether the command loads config; the * only effect is a suppressed notice or extra debug diagnostic either way. */ @@ -238,7 +240,8 @@ const projectDotenvValues = Effect.fnUntraced(function* ( : [path.join(base, "supabase"), base]; for (const dir of dirs) { for (const filename of candidateDotenvFilenames(env["SUPABASE_ENV"] || "development")) { - const contents = yield* readUtf8(fs, path.join(dir, filename)).pipe(Effect.option); + // `readFileString` drops a leading byte-order mark, as the main loader's reader does. + const contents = yield* fs.readFileString(path.join(dir, filename)).pipe(Effect.option); if (Option.isNone(contents)) continue; const parsed = Result.try(() => parseDotEnv(contents.value)); // A malformed file is only reachable here when the command never @@ -438,7 +441,6 @@ const NOTICE_ENV_KEYS = [ "SUPABASE_DEBUG", "SUPABASE_WORKDIR", "SUPABASE_ENV", - "SUPABASE_EXPERIMENTAL_STACK", ]; /** @@ -449,7 +451,6 @@ export const upgradeNoticeHook = ( args: ReadonlyArray, info: { readonly cleanShowHelp: boolean; - readonly delegatedToGo: boolean; readonly workingDirectory?: string; readonly isValueTakingFlagToken: (token: string) => boolean; }, @@ -459,52 +460,46 @@ export const upgradeNoticeHook = ( HttpClient.HttpClient > = fetchLatestReleaseTag, ): Effect.Effect => - info.delegatedToGo - ? Effect.void - : Effect.gen(function* () { - const path = yield* Path.Path; - const clock = yield* Clock.Clock; - const runtime = yield* RuntimeInfo; - const env: Record = {}; - for (const key of NOTICE_ENV_KEYS) { - env[key] = Option.getOrUndefined(yield* Config.option(Config.String(key))); - } - const supabaseHome = yield* Config.option(Config.String("SUPABASE_HOME")); - const outcome = yield* runUpgradeNotice({ - env, - args, - cleanShowHelp: info.cleanShowHelp, - isValueTakingFlagToken: info.isValueTakingFlagToken, - cwd: runtime.cwd, - resolvedCwd: info.workingDirectory, - currentVersion: CLI_VERSION, - supabaseHome: resolveSupabaseHomeValue(path, supabaseHome, runtime.homeDir), - /* An unreadable mode keeps the cache out of the project; a relative `--workdir` resolves against the process cwd. */ - usesLegacyProjectState: projectStateMode( - info.workingDirectory === undefined - ? { args, cwd: runtime.cwd, env } - : { - cwd: info.workingDirectory, - env: { SUPABASE_EXPERIMENTAL_STACK: env["SUPABASE_EXPERIMENTAL_STACK"] }, - }, - ).pipe( - Effect.map((mode) => mode === "legacy"), - Effect.orElseSucceed(() => false), - ), - now: () => clock.currentTimeMillisUnsafe(), - /* Runs inside the check span, so the fetch is its child span. */ - fetchLatestTag: fetchLatestTag.pipe( - Effect.provide(FetchHttpClient.layer), - Effect.withSpan("UpgradeNotice.fetch"), - ), - writeStderr: (text) => { - process.stderr.write(text); - }, - }); - if (outcome !== undefined) { - yield* Effect.annotateCurrentSpan({ - "cache.location": outcome.cache, - "cache.hit": outcome.cacheFresh, - }); - } - }).pipe(Effect.withSpan("UpgradeNotice.check"), Effect.ignoreCause); + Effect.gen(function* () { + const path = yield* Path.Path; + const clock = yield* Clock.Clock; + const runtime = yield* RuntimeInfo; + const env: Record = {}; + for (const key of NOTICE_ENV_KEYS) { + env[key] = Option.getOrUndefined(yield* Config.option(Config.String(key))); + } + const supabaseHome = yield* Config.option(Config.String("SUPABASE_HOME")); + const outcome = yield* runUpgradeNotice({ + env, + args, + cleanShowHelp: info.cleanShowHelp, + isValueTakingFlagToken: info.isValueTakingFlagToken, + cwd: runtime.cwd, + resolvedCwd: info.workingDirectory, + currentVersion: CLI_VERSION, + supabaseHome: resolveSupabaseHomeValue(path, supabaseHome, runtime.homeDir), + /* An unreadable mode keeps the cache out of the project. */ + usesLegacyProjectState: projectStateMode({ + args: info.workingDirectory === undefined ? args : ["--workdir", info.workingDirectory], + cwd: runtime.cwd, + }).pipe( + Effect.map((mode) => mode === "legacy"), + Effect.orElseSucceed(() => false), + ), + now: () => clock.currentTimeMillisUnsafe(), + /* Runs inside the check span, so the fetch is its child span. */ + fetchLatestTag: fetchLatestTag.pipe( + Effect.provide(FetchHttpClient.layer), + Effect.withSpan("UpgradeNotice.fetch"), + ), + writeStderr: (text) => { + process.stderr.write(text); + }, + }); + if (outcome !== undefined) { + yield* Effect.annotateCurrentSpan({ + "cache.location": outcome.cache, + "cache.hit": outcome.cacheFresh, + }); + } + }).pipe(Effect.withSpan("UpgradeNotice.check"), Effect.ignoreCause); diff --git a/apps/cli/src/command-internal/upgrade-notice.unit.test.ts b/apps/cli/src/command-internal/upgrade-notice.unit.test.ts index 04a152e3e6..c215641698 100644 --- a/apps/cli/src/command-internal/upgrade-notice.unit.test.ts +++ b/apps/cli/src/command-internal/upgrade-notice.unit.test.ts @@ -20,7 +20,7 @@ import { } from "./upgrade-notice.ts"; describe("updateNotifierDisabled", () => { - it.each(["1", "t", "T", "TRUE", "true", "True"])("suppresses for ParseBool-true %j", (value) => { + it.each(["1", "t", "T", "TRUE", "true", "True"])("suppresses for boolean-true %j", (value) => { expect(updateNotifierDisabled(value)).toBe(true); }); @@ -65,7 +65,7 @@ describe("isNewerCliVersion", () => { }); describe("formatUpgradeNotice", () => { - it("matches the Go CLI's message bytes modulo styling", () => { + it("emits the expected message bytes modulo styling", () => { expect(stripVTControlCharacters(formatUpgradeNotice("v2.114.0", "2.113.0"))).toBe( "A new version of Supabase CLI is available: v2.114.0 (currently installed v2.113.0)\n" + "We recommend updating regularly for new features and bug fixes: " + @@ -756,6 +756,19 @@ layer(BunServices.layer)("runUpgradeNotice", (it) => { }), ); + it.effect("a project dotenv with a byte-order mark still opts out", () => + Effect.gen(function* () { + const ctx = yield* setup({}); + yield* ctx.fs.writeFileString( + ctx.path.join(ctx.workdir, "supabase", ".env"), + `${String.fromCharCode(0xfeff)}SUPABASE_NO_UPDATE_NOTIFIER=1\n`, + ); + yield* notice(ctx.deps); + expect(ctx.stderr).toBe(""); + expect(ctx.fetchCalls).toBe(0); + }), + ); + it.effect("keeps a leading BOM in the cached tag, so it never reads as a release", () => Effect.gen(function* () { const ctx = yield* setup({ cacheContent: "\uFEFFv99.99.99", cacheAgeMs: 60 * 1000 }); @@ -837,17 +850,8 @@ const captureStderr = (body: Effect.Effect) => }), ); -/** - * `delegatedToGo` is asserted directly rather than via a real proxied - * command, so this doesn't depend on which commands still delegate to Go — - * that set only shrinks (`docs/go-cli-porting-status.md`), and a test pinned - * to one command would break the moment it's ported. - */ layer(hookLayer)("upgradeNoticeHook", (it) => { - const runHook = Effect.fnUntraced(function* (opts: { - readonly delegatedToGo?: boolean; - readonly cached?: boolean; - }) { + const runHook = Effect.fnUntraced(function* (opts: { readonly cached?: boolean }) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const workdir = yield* fs.makeTempDirectoryScoped({ prefix: "supabase-upgrade-notice-hook-" }); @@ -875,7 +879,6 @@ layer(hookLayer)("upgradeNoticeHook", (it) => { ["db", "branch", "list"], { cleanShowHelp: false, - delegatedToGo: opts.delegatedToGo === true, workingDirectory: workdir, isValueTakingFlagToken: () => false, }, @@ -895,12 +898,6 @@ layer(hookLayer)("upgradeNoticeHook", (it) => { }), ); - it.effect("stays silent when the run delegated to Go, which printed its own notice", () => - Effect.gen(function* () { - expect((yield* runHook({ delegatedToGo: true })).stderr).toBe(""); - }), - ); - it.effect("traces a cache hit on the check span without a fetch span", () => Effect.gen(function* () { const { span } = yield* runHook({}); @@ -974,7 +971,6 @@ layer(hookLayer)("upgradeNoticeHook release lookup", (it) => { }, upgradeNoticeHook(["db", "branch", "list"], { cleanShowHelp: false, - delegatedToGo: false, workingDirectory: opts.workdirFromEnv === true ? undefined : workdir, isValueTakingFlagToken: () => false, }), diff --git a/apps/cli/src/command-internal/workdir-project.ts b/apps/cli/src/command-internal/workdir-project.ts index 9e73fbeeae..507484650d 100644 --- a/apps/cli/src/command-internal/workdir-project.ts +++ b/apps/cli/src/command-internal/workdir-project.ts @@ -6,6 +6,7 @@ import { type CliErrorActionabilityDeclaration, ErrorActionabilityId, } from "../shared/telemetry/error-actionability.ts"; +import { configParseCauseMessage } from "../shared/output/normalize-error.ts"; import { sanitizeInlineName } from "./http-errors.ts"; /** Renders a config-load path relative to `workdir` (e.g. `supabase/config.json`), regardless of invocation cwd. */ @@ -13,6 +14,13 @@ export function relativeConfigPath(workdir: string, path: string): string { return path.startsWith(workdir) ? path.slice(workdir.length).replace(/^[/\\]/, "") : path; } +export function describeConfigParseError( + workdir: string, + error: { readonly path: string; readonly cause: unknown }, +): string { + return `failed to parse ${relativeConfigPath(workdir, error.path)}: ${configParseCauseMessage(error.cause)}`; +} + /** * Shared "no project here" message for JSON-capable config loads (`config diff/push/pull`, * `gen types`, `storage ls|mv|rm|cp`, `seed buckets`). @@ -70,8 +78,8 @@ class WorkdirProjectMissingError extends Data.TaggedError("WorkdirProjectMissing * Fails when an explicit `--workdir`/`SUPABASE_WORKDIR` holds no project; a no-op for a defaulted * workdir. * - * Only valid for callers that don't pass `tomlOnly: true` to `loadCliConfig` — that mode can - * return `null` for a `config.json`-only project even though this probe succeeds. + * Valid for any caller that loads config through the resolved config, which probes `config.json` and + * `config.toml` alike. */ export const requireExplicitWorkdirProject = Effect.fnUntraced(function* (cliSettings: { readonly workdir: string; diff --git a/apps/cli/src/command-internal/workdir-project.unit.test.ts b/apps/cli/src/command-internal/workdir-project.unit.test.ts index e9beef0fd0..d82095139a 100644 --- a/apps/cli/src/command-internal/workdir-project.unit.test.ts +++ b/apps/cli/src/command-internal/workdir-project.unit.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { missingProjectConfigMessage } from "./workdir-project.ts"; +import { describeConfigParseError, missingProjectConfigMessage } from "./workdir-project.ts"; describe("missingProjectConfigMessage", () => { it("points at `supabase init` for a defaulted workdir", () => { @@ -29,3 +29,20 @@ describe("missingProjectConfigMessage", () => { expect(message).not.toContain(""); }); }); + +describe("describeConfigParseError", () => { + it("names the failed file relative to the workdir", () => { + expect( + describeConfigParseError("/repo", { + path: "/repo/supabase/config.toml", + cause: new Error("unexpected end of input"), + }), + ).toBe("failed to parse supabase/config.toml: unexpected end of input"); + }); + + it("keeps a path outside the workdir as given", () => { + expect( + describeConfigParseError("/repo", { path: "/elsewhere/config.json", cause: "bad json" }), + ).toBe("failed to parse /elsewhere/config.json: bad json"); + }); +}); diff --git a/apps/cli/src/command-internal/workdir-search.ts b/apps/cli/src/command-internal/workdir-search.ts deleted file mode 100644 index 70619008fe..0000000000 --- a/apps/cli/src/command-internal/workdir-search.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Whether a config load should climb ancestor directories for `supabase/config.{toml,json}` - * beyond `CommandSettings.workdir`. - * - * An explicit workdir is authoritative and never climbs, so an unrelated ancestor project's - * config can't win. A defaulted workdir still climbs, since its own default resolution only - * probes `config.toml`. - */ -export function shouldSearchAncestors(cliSettings: { readonly explicitWorkdir: boolean }): boolean { - return !cliSettings.explicitWorkdir; -} diff --git a/apps/cli/src/commands/backups/list/SIDE_EFFECTS.md b/apps/cli/src/commands/backups/list/SIDE_EFFECTS.md index 905f8cfe77..84c1630ae8 100644 --- a/apps/cli/src/commands/backups/list/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/backups/list/SIDE_EFFECTS.md @@ -1,5 +1,7 @@ # `supabase backups list` +Target: hosted, no consent — see [Command target](../../../../docs/command-target.md) + ## Files Read | Path | Format | When | @@ -8,7 +10,7 @@ | keyring `"Supabase CLI"` / `` | OS keychain | when `SUPABASE_ACCESS_TOKEN` unset and keyring available; account = `CommandSettings.profile` | | keyring `"Supabase CLI"` / `access-token` | OS keychain | legacy-key fallback when the profile-keyed lookup misses | | `~/.supabase/access-token` | plain text (token string) | last-resort fallback after env + keyring miss | -| `/supabase/.temp/project-ref` | plain text | when `--project-ref` and `SUPABASE_PROJECT_ID` are both unset | +| `/supabase/.temp/project-ref` | plain text | when neither `--remote=` nor `SUPABASE_PROJECT_ID` is set | ## Files Written @@ -19,38 +21,40 @@ ## API Routes -| Method | Path | Auth | Request body | Response (used fields) | -| ------ | ------------------------------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `GET` | `/v1/projects/{ref}/database/backups` | Bearer token | none | `{region, walg_enabled, pitr_enabled, backups: [{inserted_at, status, is_physical_backup}], physical_backup_data: {earliest_physical_backup_date_unix?, latest_physical_backup_date_unix?}}` | -| `GET` | `/v1/projects` | Bearer token | none | `[{id, ref, name, organization_slug, region, ...}]` — TTY-prompt fallback only | -| `GET` | `/v1/projects/{ref}` | Bearer token | none | `{ref, name, organization_id, organization_slug}` — linked-project cache miss only (best-effort, after the command) | +| Method | Path | Auth | Request body | Response (used fields) | +| ------ | ------------------------------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `GET` | `/v1/projects/{ref}/database/backups` | Bearer token | none | `{region, walg_enabled, pitr_enabled, backups: [{inserted_at, status, is_physical_backup}], physical_backup_data: {earliest_physical_backup_date_unix?, latest_physical_backup_date_unix?}}` | +| `GET` | `/v1/projects` | Bearer token | none | `[{id, ref, name, organization_slug, region, ...}]` — TTY-prompt fallback only | +| `GET` | `/v1/projects/{ref}` | Bearer token | none | `{ref, name, organization_id, organization_slug}` — linked-project cache miss only (best-effort, after the command) | +| `GET` | `/v1/projects/{parent}/branches` | Bearer token | none | `[{id, name, project_ref, is_default}]` — only for `--remote=` (or the deprecated `--project-ref=`), and for a bare `--remote` followed by a word that could name a branch; `{parent}` is `SUPABASE_PROJECT_ID`, else the linked project | +| `GET` | `/v1/branches/{id}` | Bearer token | none | `{ref, status, …}` — only for `--remote=` when no project is linked | ## Environment Variables -| Variable | Purpose | Required? | -| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -| `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring → `~/.supabase/access-token`) | -| `SUPABASE_PROFILE` | selects API base URL: `supabase` → `api.supabase.com`, `supabase-staging` → `api.supabase.green`, `supabase-local` → `http://localhost:8080`. May alternatively be a filesystem path to a YAML profile with at least `api_url:` and optional `name:` (used by the cli-e2e test harness). | no (defaults to `supabase`) | -| `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (also reads `/supabase/.temp/project-ref` then prompts on TTY) | -| `SUPABASE_WORKDIR` | base directory for the `.temp/project-ref` lookup | no (walks up from CWD looking for `supabase/config.toml`) | +| Variable | Purpose | Required? | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring → `~/.supabase/access-token`) | +| `SUPABASE_PROFILE` | selects API base URL: `supabase` → `api.supabase.com`, `supabase-staging` → `api.supabase.green`, `supabase-local` → `http://localhost:8080`. May alternatively be a filesystem path to a YAML profile with at least `api_url:` and optional `name:` (used by the cli-e2e test harness). | no (defaults to `supabase`) | +| `SUPABASE_PROJECT_ID` | project ref fallback when `--remote=` is not given | no (also reads `/supabase/.temp/project-ref` then prompts on TTY) | +| `SUPABASE_WORKDIR` | base directory for the `.temp/project-ref` lookup | no (walks up from CWD looking for `supabase/config.toml` or `supabase/config.json`) | ## Exit Codes -| Code | Condition | -| ---- | --------------------------------------------------------------------------------------- | -| `0` | success — backup list printed to stdout | -| `1` | `AccessTokenRequiredError` — no token in env/keyring/file | -| `1` | `InvalidAccessTokenError` — token violates `^sbp_(oauth_\|v0_)?[a-f0-9]{40}$` | -| `1` | `ProjectRefNotLinkedError` — `--project-ref` unset, env/file empty, and stdin not a TTY | -| `1` | `InvalidProjectRefError` — resolved ref violates `^[a-z]{20}$` | -| `1` | `BackupListUnexpectedStatusError` — non-2xx response from the backups endpoint | -| `1` | `BackupListNetworkError` — transport-level network failure | +| Code | Condition | +| ---- | ------------------------------------------------------------------------------------- | +| `0` | success — backup list printed to stdout | +| `1` | `AccessTokenRequiredError` — no token in env/keyring/file | +| `1` | `InvalidAccessTokenError` — token violates `^sbp_(oauth_\|v0_)?[a-f0-9]{40}$` | +| `1` | `ProjectRefNotLinkedError` — no `--remote=`, env/file empty, and stdin not a TTY | +| `1` | `InvalidProjectRefError` — resolved ref violates `^[a-z]{20}$` | +| `1` | `BackupListUnexpectedStatusError` — non-2xx response from the backups endpoint | +| `1` | `BackupListNetworkError` — transport-level network failure | ## Telemetry Events Fired -| Event | When | Notable properties / groups | -| ---------------------- | ------------------------------------------ | -------------------------------------------------------------------- | -| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--project-ref` → ``) | +| Event | When | Notable properties / groups | +| ---------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--remote=` → the ref, a bare `--remote` → `true`, any other `--remote` value → ``; `--project-ref` → the ref when ref-shaped, else ``) | ## Output @@ -58,7 +62,7 @@ The `--output {pretty,json,yaml,toml,env}` flag and the `--output-format {text,j ### `--output pretty` (default) / `--output-format text` -For PITR-only projects, prints a Glamour-styled markdown table with columns: `REGION`, `WALG`, `PITR`, `EARLIEST TIMESTAMP`, `LATEST TIMESTAMP`. For projects with logical/physical backups, prints columns: `REGION`, `BACKUP TYPE`, `STATUS`, `CREATED AT (UTC)`. The table is rendered byte-for-byte using `glamour.WithStandardStyle(styles.AsciiStyle)`. +For PITR-only projects, prints a styled markdown table with columns: `REGION`, `WALG`, `PITR`, `EARLIEST TIMESTAMP`, `LATEST TIMESTAMP`. For projects with logical/physical backups, prints columns: `REGION`, `BACKUP TYPE`, `STATUS`, `CREATED AT (UTC)`. ### `--output json` @@ -91,6 +95,5 @@ One `result` NDJSON event on success containing the backup response object. ## Notes -- Requires `--project-ref`, `SUPABASE_PROJECT_ID`, a populated `/supabase/.temp/project-ref` file, or a TTY for the interactive project picker. - The interactive picker calls `GET /v1/projects` and writes `"Selected project: "` to stderr in text mode. It does **not** persist the choice; only `supabase link` and `supabase bootstrap` write the temp file. - Sends `User-Agent: SupabaseCLI/` and Bearer auth. No `X-Supabase-Command` headers. diff --git a/apps/cli/src/commands/backups/list/list.command.ts b/apps/cli/src/commands/backups/list/list.command.ts index eb28f1d634..02262f2b0c 100644 --- a/apps/cli/src/commands/backups/list/list.command.ts +++ b/apps/cli/src/commands/backups/list/list.command.ts @@ -1,20 +1,20 @@ import type * as CliCommand from "effect/cli/Command"; -import { Command, Flag } from "effect/cli"; +import { Command } from "effect/cli"; import { withPermissions } from "../../../command-internal/command-permissions/command-permissions.annotation.ts"; import { compose } from "../../../command-internal/command-permissions/model.ts"; -import { projectRefPicker } from "../../../config/remote-target.layer.ts"; +import { projectRefPicker } from "../../../config/project-ref.layer.ts"; import { linkedProjectCache } from "../../../telemetry/linked-project-cache.layer.ts"; import { withJsonErrorHandling } from "../../../shared/output/json-error-handling.ts"; import { managementApiRuntimeLayer } from "../../../command-internal/management-api-runtime.layer.ts"; import { withCommandTelemetry } from "../../../telemetry/command-telemetry.ts"; import { backupsList } from "./list.handler.ts"; +import { withTargetPolicy } from "../../../command-internal/target/target-policy.ts"; +import { branchResolution } from "../../../command-internal/target/branch-resolution.ts"; +import { projectTargetFlags } from "../../../command-internal/target/target-flags.ts"; const config = { - projectRef: Flag.String("project-ref").pipe( - Flag.withDescription("Project ref of the Supabase project."), - Flag.optional, - ), + ...projectTargetFlags(), } as const; export type BackupsListFlags = CliCommand.Command.Config.Infer; @@ -28,7 +28,7 @@ export const backupsListCommand = Command.make("list", config).pipe( description: "List all physical backups", }, { - command: "supabase backups list --project-ref ", + command: "supabase backups list --remote=", description: "List backups for a specific project", }, ]), @@ -37,10 +37,12 @@ export const backupsListCommand = Command.make("list", config).pipe( { operations: [{ operationId: "v1-list-all-backups", kind: "required" }] }, projectRefPicker, linkedProjectCache, + branchResolution, ), ), Command.withHandler((flags) => backupsList(flags).pipe(withCommandTelemetry({ flags }), withJsonErrorHandling), ), Command.provide(managementApiRuntimeLayer(["backups", "list"])), + withTargetPolicy({ group: "hosted", consent: "none" }), ); diff --git a/apps/cli/src/commands/backups/list/list.handler.ts b/apps/cli/src/commands/backups/list/list.handler.ts index 0587fe9d73..70089a15a1 100644 --- a/apps/cli/src/commands/backups/list/list.handler.ts +++ b/apps/cli/src/commands/backups/list/list.handler.ts @@ -2,52 +2,52 @@ import type { V1ListAllBackupsOutput } from "@supabase/api/effect"; import { Effect, Option } from "effect"; import { CommandPlatformApi } from "../../../auth/command-platform-api.service.ts"; -import { RemoteTarget } from "../../../config/remote-target.service.ts"; +import { resolveProjectTarget } from "../../../command-internal/target/resolve-project-target.ts"; import { LinkedProjectCache } from "../../../telemetry/linked-project-cache.service.ts"; import { TelemetryState } from "../../../telemetry/telemetry-state.service.ts"; import { OutputFlag } from "../../../command-internal/global-flags.ts"; import { Output } from "../../../shared/output/output.service.ts"; import { renderGlamourTable } from "../../../output/glamour-table.ts"; import { BackupListNetworkError, BackupListUnexpectedStatusError } from "../backups.errors.ts"; -import { encodeEnv, encodeGoJson } from "../../../command-internal/go-output.encoders.ts"; +import { encodeEnv, encodeSortedJson } from "../../../command-internal/output.encoders.ts"; import { - encodeGoToml, - encodeGoYaml, - goBool, - goInt, - goPtr, - goSlice, - goString, - goStruct, -} from "../../../command-internal/go-struct-output.encoders.ts"; + encodeStructToml, + encodeStructYaml, + shapeBool, + shapeInt, + shapePtr, + shapeSlice, + shapeString, + shapeStruct, +} from "../../../command-internal/struct-output.encoders.ts"; import { mapHttpError } from "../../../command-internal/http-errors.ts"; import { formatTimestamp } from "../../../command-internal/timestamp.format.ts"; import { formatRegion } from "../../../command-internal/region.format.ts"; import type { BackupsListFlags } from "./list.command.ts"; /** Struct shape for `-o yaml|toml` encoding of the backups response. */ -const GO_BACKUPS_RESPONSE = goStruct([ +const BACKUPS_RESPONSE_SHAPE = shapeStruct([ [ "backups", - goSlice( - goStruct([ - ["id", goInt], - ["inserted_at", goString], - ["is_physical_backup", goBool], - ["status", goString], + shapeSlice( + shapeStruct([ + ["id", shapeInt], + ["inserted_at", shapeString], + ["is_physical_backup", shapeBool], + ["status", shapeString], ]), ), ], [ "physical_backup_data", - goStruct([ - ["earliest_physical_backup_date_unix", goPtr(goInt)], - ["latest_physical_backup_date_unix", goPtr(goInt)], + shapeStruct([ + ["earliest_physical_backup_date_unix", shapePtr(shapeInt)], + ["latest_physical_backup_date_unix", shapePtr(shapeInt)], ]), ], - ["pitr_enabled", goBool], - ["region", goString], - ["walg_enabled", goBool], + ["pitr_enabled", shapeBool], + ["region", shapeString], + ["walg_enabled", shapeBool], ]); type BackupsResponse = typeof V1ListAllBackupsOutput.Type; @@ -91,13 +91,12 @@ function renderLogicalTable(response: BackupsResponse): string { export const backupsList = Effect.fn("backups.list")(function* (flags: BackupsListFlags) { const output = yield* Output; - const goOutputFlag = yield* OutputFlag; + const outputFlag = yield* OutputFlag; const api = yield* CommandPlatformApi; - const resolver = yield* RemoteTarget; const linkedProjectCache = yield* LinkedProjectCache; const telemetryState = yield* TelemetryState; - const { ref } = yield* resolver.resolve(flags.projectRef); + const ref = yield* resolveProjectTarget(flags); yield* Effect.gen(function* () { // Spinner is text-mode only; in JSON/stream-json it would leave a dangling `[task] start:` @@ -110,31 +109,31 @@ export const backupsList = Effect.fn("backups.list")(function* (flags: BackupsLi ); yield* fetching?.clear ?? Effect.void; - const goFmt = Option.getOrUndefined(goOutputFlag); + const outputFlagFormat = Option.getOrUndefined(outputFlag); - if (goFmt === "json") { - yield* output.raw(encodeGoJson(response, { nullForEmptyArrays: ["backups"] })); + if (outputFlagFormat === "json") { + yield* output.raw(encodeSortedJson(response, { nullForEmptyArrays: ["backups"] })); return; } - if (goFmt === "yaml") { - yield* output.raw(encodeGoYaml(response, GO_BACKUPS_RESPONSE)); + if (outputFlagFormat === "yaml") { + yield* output.raw(encodeStructYaml(response, BACKUPS_RESPONSE_SHAPE)); return; } - if (goFmt === "toml") { + if (outputFlagFormat === "toml") { // Treats an empty backups list as absent, matching the nullForEmptyArrays JSON handling // above. yield* output.raw( - encodeGoToml( + encodeStructToml( { ...response, backups: response.backups.length > 0 ? response.backups : undefined, }, - GO_BACKUPS_RESPONSE, + BACKUPS_RESPONSE_SHAPE, ), ); return; } - if (goFmt === "env") { + if (outputFlagFormat === "env") { yield* output.raw(encodeEnv(response) + "\n"); return; } diff --git a/apps/cli/src/commands/backups/list/list.integration.test.ts b/apps/cli/src/commands/backups/list/list.integration.test.ts index 7b844fa782..2eb3f1a72e 100644 --- a/apps/cli/src/commands/backups/list/list.integration.test.ts +++ b/apps/cli/src/commands/backups/list/list.integration.test.ts @@ -12,6 +12,7 @@ import { mockCommandPlatformApi, useTempWorkdir, } from "../../../../tests/helpers/command-mocks.ts"; +import { describeHostedTargetSelection } from "../../../../tests/helpers/hosted-target-selection.ts"; import { assertPermissionDrift } from "../../../../tests/helpers/permission-drift.ts"; import { realLinkedProjectCache } from "../../../../tests/helpers/permission-real-layers.ts"; import { backupsList } from "./list.handler.ts"; @@ -60,12 +61,13 @@ const LOGICAL_RESPONSE: typeof V1ListAllBackupsOutput.Type = { interface SetupOpts { format?: "text" | "json" | "stream-json"; - goOutput?: "env" | "pretty" | "json" | "toml" | "yaml"; + outputFlag?: "env" | "pretty" | "json" | "toml" | "yaml"; response?: typeof V1ListAllBackupsOutput.Type; status?: number; network?: "fail"; apiUrl?: string; userAgent?: string; + projectId?: string; } const tempRoot = useTempWorkdir("supabase-backups-list-int-"); @@ -82,12 +84,13 @@ function setup(opts: SetupOpts = {}) { workdir: tempRoot.current, apiUrl: opts.apiUrl, userAgent: opts.userAgent, + ...(opts.projectId === undefined ? {} : { projectId: Option.some(opts.projectId) }), }); const layer = buildTestRuntime({ out, api, cliSettings, - goOutput: opts.goOutput === undefined ? Option.none() : Option.some(opts.goOutput), + outputFlag: opts.outputFlag === undefined ? Option.none() : Option.some(opts.outputFlag), }); return { layer, out, api }; } @@ -96,7 +99,7 @@ describe("backups list integration", () => { it.effect("renders a PITR-only table when no physical backups exist", () => { const { layer, out } = setup({ response: PITR_RESPONSE }); return Effect.gen(function* () { - yield* backupsList({ projectRef: Option.none() }); + yield* backupsList({ remote: Option.none(), projectRef: Option.none() }); expect(out.stdoutText).toContain("REGION"); expect(out.stdoutText).toContain("WALG"); expect(out.stdoutText).toContain("PITR"); @@ -110,7 +113,7 @@ describe("backups list integration", () => { it.effect("renders a logical backups table with PHYSICAL classification", () => { const { layer, out } = setup({ response: LOGICAL_RESPONSE }); return Effect.gen(function* () { - yield* backupsList({ projectRef: Option.none() }); + yield* backupsList({ remote: Option.none(), projectRef: Option.none() }); expect(out.stdoutText).toContain("BACKUP TYPE"); expect(out.stdoutText).toContain("PHYSICAL"); expect(out.stdoutText).toContain("COMPLETED"); @@ -121,7 +124,7 @@ describe("backups list integration", () => { it.effect("translates ap-southeast-1 to Southeast Asia (Singapore)", () => { const { layer, out } = setup({ response: PITR_RESPONSE }); return Effect.gen(function* () { - yield* backupsList({ projectRef: Option.none() }); + yield* backupsList({ remote: Option.none(), projectRef: Option.none() }); expect(out.stdoutText).toContain("Southeast Asia (Singapore)"); }).pipe(Effect.provide(layer)); }); @@ -129,7 +132,7 @@ describe("backups list integration", () => { it.effect("emits a JSON success event when --output-format=json", () => { const { layer, out } = setup({ format: "json", response: PITR_RESPONSE }); return Effect.gen(function* () { - yield* backupsList({ projectRef: Option.none() }); + yield* backupsList({ remote: Option.none(), projectRef: Option.none() }); const success = out.messages.find((m) => m.type === "success"); expect(success).toBeDefined(); expect(success?.data).toMatchObject({ region: "ap-southeast-1", walg_enabled: true }); @@ -139,17 +142,17 @@ describe("backups list integration", () => { it.effect("emits a result event for --output-format=stream-json", () => { const { layer, out } = setup({ format: "stream-json", response: PITR_RESPONSE }); return Effect.gen(function* () { - yield* backupsList({ projectRef: Option.none() }); + yield* backupsList({ remote: Option.none(), projectRef: Option.none() }); const success = out.messages.find((m) => m.type === "success"); expect(success).toBeDefined(); expect(success?.data).toMatchObject({ region: "ap-southeast-1" }); }).pipe(Effect.provide(layer)); }); - it.effect("emits indented JSON to stdout for --output json (Go-compat)", () => { - const { layer, out } = setup({ goOutput: "json", response: PITR_RESPONSE }); + it.effect("emits indented JSON to stdout for --output json", () => { + const { layer, out } = setup({ outputFlag: "json", response: PITR_RESPONSE }); return Effect.gen(function* () { - yield* backupsList({ projectRef: Option.none() }); + yield* backupsList({ remote: Option.none(), projectRef: Option.none() }); expect(out.stdoutText).toBe( `{ "backups": null, @@ -164,27 +167,27 @@ describe("backups list integration", () => { }); it.effect("emits YAML to stdout for --output yaml", () => { - const { layer, out } = setup({ goOutput: "yaml", response: PITR_RESPONSE }); + const { layer, out } = setup({ outputFlag: "yaml", response: PITR_RESPONSE }); return Effect.gen(function* () { - yield* backupsList({ projectRef: Option.none() }); + yield* backupsList({ remote: Option.none(), projectRef: Option.none() }); expect(out.stdoutText).toContain("region: ap-southeast-1"); expect(out.stdoutText).toContain("walgenabled: true"); }).pipe(Effect.provide(layer)); }); it.effect("emits TOML to stdout for --output toml", () => { - const { layer, out } = setup({ goOutput: "toml", response: PITR_RESPONSE }); + const { layer, out } = setup({ outputFlag: "toml", response: PITR_RESPONSE }); return Effect.gen(function* () { - yield* backupsList({ projectRef: Option.none() }); + yield* backupsList({ remote: Option.none(), projectRef: Option.none() }); expect(out.stdoutText).toContain('Region = "ap-southeast-1"'); expect(out.stdoutText).toContain("WalgEnabled = true"); }).pipe(Effect.provide(layer)); }); it.effect("emits [[Backups]] array-of-tables for --output toml with logical backups", () => { - const { layer, out } = setup({ goOutput: "toml", response: LOGICAL_RESPONSE }); + const { layer, out } = setup({ outputFlag: "toml", response: LOGICAL_RESPONSE }); return Effect.gen(function* () { - yield* backupsList({ projectRef: Option.none() }); + yield* backupsList({ remote: Option.none(), projectRef: Option.none() }); expect(out.stdoutText).toBe(`PitrEnabled = true Region = "ap-southeast-1" WalgEnabled = true @@ -201,18 +204,18 @@ WalgEnabled = true }); it.effect("emits KEY=VALUE lines for --output env", () => { - const { layer, out } = setup({ goOutput: "env", response: PITR_RESPONSE }); + const { layer, out } = setup({ outputFlag: "env", response: PITR_RESPONSE }); return Effect.gen(function* () { - yield* backupsList({ projectRef: Option.none() }); + yield* backupsList({ remote: Option.none(), projectRef: Option.none() }); expect(out.stdoutText).toContain('REGION="ap-southeast-1"'); expect(out.stdoutText).toContain('WALG_ENABLED="true"'); }).pipe(Effect.provide(layer)); }); it.effect("treats --output pretty as identical to text mode (Glamour table)", () => { - const { layer, out } = setup({ goOutput: "pretty", response: PITR_RESPONSE }); + const { layer, out } = setup({ outputFlag: "pretty", response: PITR_RESPONSE }); return Effect.gen(function* () { - yield* backupsList({ projectRef: Option.none() }); + yield* backupsList({ remote: Option.none(), projectRef: Option.none() }); expect(out.stdoutText).toContain("Southeast Asia (Singapore)"); }).pipe(Effect.provide(layer)); }); @@ -220,11 +223,11 @@ WalgEnabled = true it.effect("--output flag value wins over --output-format when both provided", () => { const { layer, out } = setup({ format: "json", - goOutput: "yaml", + outputFlag: "yaml", response: PITR_RESPONSE, }); return Effect.gen(function* () { - yield* backupsList({ projectRef: Option.none() }); + yield* backupsList({ remote: Option.none(), projectRef: Option.none() }); expect(out.stdoutText).toContain("region: ap-southeast-1"); expect(out.stdoutText.startsWith("{")).toBe(false); }).pipe(Effect.provide(layer)); @@ -233,22 +236,25 @@ WalgEnabled = true it.effect("passes the resolved project ref into the listAllBackups URL", () => { const { layer, api } = setup({ response: PITR_RESPONSE }); return Effect.gen(function* () { - yield* backupsList({ projectRef: Option.none() }); + yield* backupsList({ remote: Option.none(), projectRef: Option.none() }); expect(api.requests).toHaveLength(1); expect(api.requests[0]?.url).toContain(`/v1/projects/${VALID_REF}/database/backups`); assertPermissionDrift({ command: "backups list", requests: api.requests, exact: true }); }).pipe(Effect.provide(layer)); }); - it.effect("uses --project-ref flag value over CommandSettings.projectId env", () => { + it.effect("uses the --remote= value over CommandSettings.projectId env", () => { const flagRef = "zzzzzzzzzzzzzzzzzzzz"; const { layer, api } = setup({ response: PITR_RESPONSE }); return Effect.gen(function* () { - yield* backupsList({ projectRef: Option.some(flagRef) }); + yield* backupsList({ + remote: Option.some({ _tag: "Ref", ref: flagRef }), + projectRef: Option.none(), + }); expect(api.requests[0]?.url).toContain(`/v1/projects/${flagRef}/`); assertPermissionDrift({ command: "backups list", - activeFlags: { "project-ref": true }, + activeFlags: { remote: flagRef }, requests: api.requests, exact: true, }); @@ -288,7 +294,7 @@ WalgEnabled = true const fs = yield* FileSystem.FileSystem; const cachePath = `${tempRoot.current}/supabase/.temp/linked-project.json`; expect(yield* fs.exists(cachePath)).toBe(false); - yield* backupsList({ projectRef: Option.none() }); + yield* backupsList({ remote: Option.none(), projectRef: Option.none() }); expect(api.requests.map(({ method, url }) => `${method} ${url}`)).toEqual([ "GET https://api.supabase.com/v1/projects", `GET https://api.supabase.com/v1/projects/${VALID_REF}/database/backups`, @@ -320,7 +326,7 @@ WalgEnabled = true yield* fs.makeDirectory(tempDir, { recursive: true }); yield* fs.writeFileString(path.join(tempDir, "project-ref"), fileRef); - yield* backupsList({ projectRef: Option.none() }); + yield* backupsList({ remote: Option.none(), projectRef: Option.none() }); expect(api.requests[0]?.url).toContain(`/v1/projects/${fileRef}/`); }).pipe(Effect.provide(layer)); }); @@ -336,7 +342,9 @@ WalgEnabled = true return Effect.gen(function* () { const exit = yield* Effect.exit( - backupsList({ projectRef: Option.none() }).pipe(Effect.provide(layer)), + backupsList({ remote: Option.none(), projectRef: Option.none() }).pipe( + Effect.provide(layer), + ), ); expect(Exit.isFailure(exit)).toBe(true); if (Exit.isFailure(exit)) { @@ -346,9 +354,11 @@ WalgEnabled = true }); it.effect("fails with InvalidProjectRefError when the resolved ref is malformed", () => { - const { layer } = setup({ response: PITR_RESPONSE }); + const { layer } = setup({ response: PITR_RESPONSE, projectId: "BADREF" }); return Effect.gen(function* () { - const exit = yield* Effect.exit(backupsList({ projectRef: Option.some("BADREF") })); + const exit = yield* Effect.exit( + backupsList({ remote: Option.none(), projectRef: Option.none() }), + ); expect(Exit.isFailure(exit)).toBe(true); if (Exit.isFailure(exit)) { expect(Cause.pretty(exit.cause)).toContain("InvalidProjectRefError"); @@ -359,7 +369,9 @@ WalgEnabled = true it.effect("fails with BackupListUnexpectedStatusError on HTTP 503", () => { const { layer } = setup({ status: 503, response: PITR_RESPONSE }); return Effect.gen(function* () { - const exit = yield* Effect.exit(backupsList({ projectRef: Option.none() })); + const exit = yield* Effect.exit( + backupsList({ remote: Option.none(), projectRef: Option.none() }), + ); expect(Exit.isFailure(exit)).toBe(true); if (Exit.isFailure(exit)) { const causeText = Cause.pretty(exit.cause); @@ -372,7 +384,9 @@ WalgEnabled = true it.effect("fails with BackupListNetworkError on transport failure", () => { const { layer } = setup({ network: "fail", response: PITR_RESPONSE }); return Effect.gen(function* () { - const exit = yield* Effect.exit(backupsList({ projectRef: Option.none() })); + const exit = yield* Effect.exit( + backupsList({ remote: Option.none(), projectRef: Option.none() }), + ); expect(Exit.isFailure(exit)).toBe(true); if (Exit.isFailure(exit)) { const causeText = Cause.pretty(exit.cause); @@ -385,25 +399,29 @@ WalgEnabled = true it.effect("emits a fail event when withJsonErrorHandling wraps a JSON-mode error", () => { const { layer, out } = setup({ format: "json", status: 503, response: PITR_RESPONSE }); return Effect.gen(function* () { - yield* backupsList({ projectRef: Option.none() }).pipe(withJsonErrorHandling); + yield* backupsList({ remote: Option.none(), projectRef: Option.none() }).pipe( + withJsonErrorHandling, + ); expect(out.messages.some((m) => m.type === "fail")).toBe(true); }).pipe(Effect.provide(layer)); }); - it.effect( - "sends User-Agent SupabaseCLI/ and no X-Supabase-Command headers (Go parity)", - () => { - const { layer, api } = setup({ - response: PITR_RESPONSE, - userAgent: "SupabaseCLI/1.42.0", - }); - return Effect.gen(function* () { - yield* backupsList({ projectRef: Option.none() }); - const headers = api.requests[0]?.headers; - expect(headers?.["user-agent"]).toBe("SupabaseCLI/1.42.0"); - expect(headers?.["x-supabase-command"]).toBeUndefined(); - expect(headers?.["x-supabase-command-run-id"]).toBeUndefined(); - }).pipe(Effect.provide(layer)); - }, - ); + it.effect("sends User-Agent SupabaseCLI/ and no X-Supabase-Command headers", () => { + const { layer, api } = setup({ + response: PITR_RESPONSE, + userAgent: "SupabaseCLI/1.42.0", + }); + return Effect.gen(function* () { + yield* backupsList({ remote: Option.none(), projectRef: Option.none() }); + const headers = api.requests[0]?.headers; + expect(headers?.["user-agent"]).toBe("SupabaseCLI/1.42.0"); + expect(headers?.["x-supabase-command"]).toBeUndefined(); + expect(headers?.["x-supabase-command-run-id"]).toBeUndefined(); + }).pipe(Effect.provide(layer)); + }); +}); + +describeHostedTargetSelection("backups list", { + respond: (request) => Effect.succeed(jsonResponse(request, 200, PITR_RESPONSE)), + run: (flags) => backupsList(flags), }); diff --git a/apps/cli/src/commands/backups/restore/SIDE_EFFECTS.md b/apps/cli/src/commands/backups/restore/SIDE_EFFECTS.md index 82ed519c38..b0a3ef4867 100644 --- a/apps/cli/src/commands/backups/restore/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/backups/restore/SIDE_EFFECTS.md @@ -1,5 +1,7 @@ # `supabase backups restore` +Target: hosted, consent default No — see [Command target](../../../../docs/command-target.md) + ## Files Read | Path | Format | When | @@ -8,7 +10,7 @@ | keyring `"Supabase CLI"` / `` | OS keychain | when `SUPABASE_ACCESS_TOKEN` unset and keyring available; account = `CommandSettings.profile` | | keyring `"Supabase CLI"` / `access-token` | OS keychain | legacy-key fallback when the profile-keyed lookup misses | | `~/.supabase/access-token` | plain text (token string) | last-resort fallback after env + keyring miss | -| `/supabase/.temp/project-ref` | plain text | when `--project-ref` and `SUPABASE_PROJECT_ID` are both unset | +| `/supabase/.temp/project-ref` | plain text | when neither `--remote=` nor `SUPABASE_PROJECT_ID` is set | ## Files Written @@ -19,38 +21,43 @@ ## API Routes -| Method | Path | Auth | Request body | Response (used fields) | -| ------ | -------------------------------------------------- | ------------ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | -| `POST` | `/v1/projects/{ref}/database/backups/restore-pitr` | Bearer token | `{recovery_time_target_unix: int64}` | none (201 Created) | -| `GET` | `/v1/projects` | Bearer token | none | `[{id, ref, name, organization_slug, region, ...}]` — TTY-prompt fallback only | -| `GET` | `/v1/projects/{ref}` | Bearer token | none | `{ref, name, organization_id, organization_slug}` — linked-project cache miss only (best-effort, after the command) | +| Method | Path | Auth | Request body | Response (used fields) | +| ------ | -------------------------------------------------- | ------------ | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `POST` | `/v1/projects/{ref}/database/backups/restore-pitr` | Bearer token | `{recovery_time_target_unix: int64}` | none (201 Created) | +| `GET` | `/v1/projects` | Bearer token | none | `[{id, ref, name, organization_slug, region, ...}]` — TTY-prompt fallback only | +| `GET` | `/v1/projects/{ref}` | Bearer token | none | `{ref, name, organization_id, organization_slug}` — linked-project cache miss only (best-effort, after the command) | +| `GET` | `/v1/projects/{parent}/branches` | Bearer token | none | `[{id, name, project_ref, is_default}]` — only for `--remote=` (or the deprecated `--project-ref=`), and for a bare `--remote` followed by a word that could name a branch; `{parent}` is `SUPABASE_PROJECT_ID`, else the linked project | +| `GET` | `/v1/branches/{id}` | Bearer token | none | `{ref, status, …}` — only for `--remote=` when no project is linked | ## Environment Variables -| Variable | Purpose | Required? | -| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -| `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring → `~/.supabase/access-token`) | -| `SUPABASE_PROFILE` | selects API base URL: `supabase` → `api.supabase.com`, `supabase-staging` → `api.supabase.green`, `supabase-local` → `http://localhost:8080`. May alternatively be a filesystem path to a YAML profile with at least `api_url:` and optional `name:` (used by the cli-e2e test harness). | no (defaults to `supabase`) | -| `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (also reads `/supabase/.temp/project-ref` then prompts on TTY) | -| `SUPABASE_WORKDIR` | base directory for the `.temp/project-ref` lookup | no (walks up from CWD looking for `supabase/config.toml`) | +| Variable | Purpose | Required? | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring → `~/.supabase/access-token`) | +| `SUPABASE_PROFILE` | selects API base URL: `supabase` → `api.supabase.com`, `supabase-staging` → `api.supabase.green`, `supabase-local` → `http://localhost:8080`. May alternatively be a filesystem path to a YAML profile with at least `api_url:` and optional `name:` (used by the cli-e2e test harness). | no (defaults to `supabase`) | +| `SUPABASE_PROJECT_ID` | project ref fallback when `--remote=` is not given | no (also reads `/supabase/.temp/project-ref` then prompts on TTY) | +| `SUPABASE_WORKDIR` | base directory for the `.temp/project-ref` lookup | no (walks up from CWD looking for `supabase/config.toml` or `supabase/config.json`) | +| `SUPABASE_YES` | approves the hosted-mutation confirmation like `--yes` (shell environment only; `supabase/.env` is ignored) | no | ## Exit Codes -| Code | Condition | -| ---- | --------------------------------------------------------------------------------------- | -| `0` | success — restore initiated | -| `1` | `AccessTokenRequiredError` — no token in env/keyring/file | -| `1` | `InvalidAccessTokenError` — token violates `^sbp_(oauth_\|v0_)?[a-f0-9]{40}$` | -| `1` | `ProjectRefNotLinkedError` — `--project-ref` unset, env/file empty, and stdin not a TTY | -| `1` | `InvalidProjectRefError` — resolved ref violates `^[a-z]{20}$` | -| `1` | `BackupRestoreUnexpectedStatusError` — non-201 response from the restore endpoint | -| `1` | `BackupRestoreNetworkError` — transport-level network failure | +| Code | Condition | +| ---- | ---------------------------------------------------------------------------------------------------------------------- | +| `0` | success — restore initiated | +| `1` | `AccessTokenRequiredError` — no token in env/keyring/file | +| `1` | `InvalidAccessTokenError` — token violates `^sbp_(oauth_\|v0_)?[a-f0-9]{40}$` | +| `1` | `ProjectRefNotLinkedError` — no `--remote=`, env/file empty, and stdin not a TTY | +| `1` | `InvalidProjectRefError` — resolved ref violates `^[a-z]{20}$` | +| `1` | `BackupRestoreUnexpectedStatusError` — non-201 response from the restore endpoint | +| `1` | `BackupRestoreNetworkError` — transport-level network failure | +| `1` | `HostedMutationConsentRequiredError` — no `--yes` / shell `SUPABASE_YES` and the run cannot prompt; nothing is changed | +| `1` | `HostedMutationDeclinedError` — the confirmation prompt was declined | ## Telemetry Events Fired -| Event | When | Notable properties / groups | -| ---------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------ | -| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--project-ref` / `--timestamp` → ``) | +| Event | When | Notable properties / groups | +| ---------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--remote=` → the ref, a bare `--remote` → `true`, any other `--remote` value → ``; `--project-ref` → the ref when ref-shaped, else ``; `--timestamp` → ``) | ## Output @@ -78,6 +85,7 @@ One `result` NDJSON event on success with the project ref payload. - `--timestamp` / `-t` accepts seconds since Unix epoch (int64). Defaults to `0`, which the API interprets as "now". - Known gap: the generated `V1RestorePitrBackupInput` schema enforces `recovery_time_target_unix >= 0`, so a negative value is rejected locally with a schema decode error instead of being forwarded to the API (the underlying field has no lower bound). Resolving this requires an upstream OpenAPI spec change. -- Requires `--project-ref`, `SUPABASE_PROJECT_ID`, a populated `/supabase/.temp/project-ref` file, or a TTY for the interactive project picker. - The interactive picker calls `GET /v1/projects` and writes `"Selected project: "` to stderr in text mode. It does **not** persist the choice; only `supabase link` and `supabase bootstrap` write the temp file. - Sends `User-Agent: SupabaseCLI/` and Bearer auth. No `X-Supabase-Command` headers. + +- **Consent.** Asks once, after the project is resolved and before the POST, with default No: `Restore the database to Unix timestamp (PITR)`. With no `--timestamp` the action reads `Start a PITR restore without a recovery time target`. diff --git a/apps/cli/src/commands/backups/restore/restore.command.ts b/apps/cli/src/commands/backups/restore/restore.command.ts index 8d72aa8819..c00007c752 100644 --- a/apps/cli/src/commands/backups/restore/restore.command.ts +++ b/apps/cli/src/commands/backups/restore/restore.command.ts @@ -3,18 +3,18 @@ import { Command, Flag } from "effect/cli"; import { withPermissions } from "../../../command-internal/command-permissions/command-permissions.annotation.ts"; import { compose } from "../../../command-internal/command-permissions/model.ts"; -import { projectRefPicker } from "../../../config/remote-target.layer.ts"; +import { projectRefPicker } from "../../../config/project-ref.layer.ts"; import { linkedProjectCache } from "../../../telemetry/linked-project-cache.layer.ts"; import { withJsonErrorHandling } from "../../../shared/output/json-error-handling.ts"; import { managementApiRuntimeLayer } from "../../../command-internal/management-api-runtime.layer.ts"; import { withCommandTelemetry } from "../../../telemetry/command-telemetry.ts"; import { backupsRestore } from "./restore.handler.ts"; +import { withTargetPolicy } from "../../../command-internal/target/target-policy.ts"; +import { branchResolution } from "../../../command-internal/target/branch-resolution.ts"; +import { projectTargetFlags } from "../../../command-internal/target/target-flags.ts"; const config = { - projectRef: Flag.String("project-ref").pipe( - Flag.withDescription("Project ref of the Supabase project."), - Flag.optional, - ), + ...projectTargetFlags(), timestamp: Flag.Int("timestamp").pipe( Flag.withAlias("t"), Flag.withDescription("The recovery time target in seconds since epoch."), @@ -41,10 +41,12 @@ export const backupsRestoreCommand = Command.make("restore", config).pipe( }, projectRefPicker, linkedProjectCache, + branchResolution, ), ), Command.withHandler((flags) => backupsRestore(flags).pipe(withCommandTelemetry({ flags }), withJsonErrorHandling), ), Command.provide(managementApiRuntimeLayer(["backups", "restore"])), + withTargetPolicy({ group: "hosted", consent: "no" }), ); diff --git a/apps/cli/src/commands/backups/restore/restore.handler.ts b/apps/cli/src/commands/backups/restore/restore.handler.ts index 86fde5e2ab..50b8345073 100644 --- a/apps/cli/src/commands/backups/restore/restore.handler.ts +++ b/apps/cli/src/commands/backups/restore/restore.handler.ts @@ -1,11 +1,12 @@ import { Effect, Option } from "effect"; import { CommandPlatformApi } from "../../../auth/command-platform-api.service.ts"; -import { RemoteTarget } from "../../../config/remote-target.service.ts"; +import { resolveProjectTarget } from "../../../command-internal/target/resolve-project-target.ts"; import { LinkedProjectCache } from "../../../telemetry/linked-project-cache.service.ts"; import { TelemetryState } from "../../../telemetry/telemetry-state.service.ts"; import { OutputFlag } from "../../../command-internal/global-flags.ts"; -import { encodeGoJson } from "../../../command-internal/go-output.encoders.ts"; +import { requireHostedMutationConsent } from "../../../command-internal/hosted-consent/hosted-consent.ts"; +import { encodeSortedJson } from "../../../command-internal/output.encoders.ts"; import { Output } from "../../../shared/output/output.service.ts"; import { BackupRestoreNetworkError, @@ -23,16 +24,21 @@ const mapRestoreError = mapHttpError({ export const backupsRestore = Effect.fn("backups.restore")(function* (flags: BackupsRestoreFlags) { const output = yield* Output; - const goOutputFlag = yield* OutputFlag; + const outputFlag = yield* OutputFlag; const api = yield* CommandPlatformApi; - const resolver = yield* RemoteTarget; const linkedProjectCache = yield* LinkedProjectCache; const telemetryState = yield* TelemetryState; - const { ref } = yield* resolver.resolve(flags.projectRef); + const ref = yield* resolveProjectTarget(flags); const recoveryTimeTargetUnix = Option.getOrElse(flags.timestamp, () => 0); yield* Effect.gen(function* () { + yield* requireHostedMutationConsent({ + action: Option.isSome(flags.timestamp) + ? `Restore the database to Unix timestamp ${flags.timestamp.value} (PITR)` + : "Start a PITR restore without a recovery time target", + }); + const restoring = output.format === "text" ? yield* output.task("Initiating PITR restore...") : undefined; yield* api.v1 @@ -43,16 +49,19 @@ export const backupsRestore = Effect.fn("backups.restore")(function* (flags: Bac ); yield* restoring?.clear ?? Effect.void; - const goFmt = Option.getOrUndefined(goOutputFlag); + const outputFlagFormat = Option.getOrUndefined(outputFlag); // --output is ignored for restore except `json`, a TS-only structured payload; every other // value (including unset) writes the text line to stderr. - if (goFmt === "json") { - yield* output.raw(encodeGoJson({ message: "Started PITR restore", project_ref: ref })); + if (outputFlagFormat === "json") { + yield* output.raw(encodeSortedJson({ message: "Started PITR restore", project_ref: ref })); return; } - if (goFmt === undefined && (output.format === "json" || output.format === "stream-json")) { + if ( + outputFlagFormat === undefined && + (output.format === "json" || output.format === "stream-json") + ) { yield* output.success("Started PITR restore", { project_ref: ref }); return; } diff --git a/apps/cli/src/commands/backups/restore/restore.integration.test.ts b/apps/cli/src/commands/backups/restore/restore.integration.test.ts index b6e9590eb5..adc1eb3c2a 100644 --- a/apps/cli/src/commands/backups/restore/restore.integration.test.ts +++ b/apps/cli/src/commands/backups/restore/restore.integration.test.ts @@ -1,6 +1,6 @@ import type { V1ListAllProjectsOutput } from "@supabase/api/effect"; import { describe, expect, it } from "@effect/vitest"; -import { Cause, Effect, Exit, FileSystem, Option, Schema } from "effect"; +import { Cause, Effect, Exit, Layer, Option, FileSystem, Schema } from "effect"; import { mockOutput, mockTty } from "../../../../tests/helpers/mocks.ts"; import { @@ -10,10 +10,15 @@ import { mockCommandSettings, mockCommandPlatformApi, useTempWorkdir, + unverifiedTargetBanner, } from "../../../../tests/helpers/command-mocks.ts"; +import { describeHostedConsent } from "../../../../tests/helpers/hosted-consent.ts"; +import { describeHostedTargetSelection } from "../../../../tests/helpers/hosted-target-selection.ts"; import { assertPermissionDrift } from "../../../../tests/helpers/permission-drift.ts"; import { realLinkedProjectCache } from "../../../../tests/helpers/permission-real-layers.ts"; import { backupsRestore } from "./restore.handler.ts"; +import { YesFlag } from "../../../command-internal/global-flags.ts"; +import { TargetPolicy } from "../../../command-internal/target/target-policy.ts"; const decodeJson = Schema.decodeEffect(Schema.fromJsonString(Schema.Unknown)); @@ -36,13 +41,22 @@ const PROJECT: (typeof V1ListAllProjectsOutput.Type)[number] = { interface SetupOpts { format?: "text" | "json" | "stream-json"; - goOutput?: "env" | "pretty" | "json" | "toml" | "yaml"; + outputFlag?: "env" | "pretty" | "json" | "toml" | "yaml"; status?: number; network?: "fail"; } const tempRoot = useTempWorkdir("supabase-backups-restore-int-"); +const buildRuntime = (opts: Parameters[0]) => + Layer.mergeAll( + buildTestRuntime(opts), + Layer.succeed(YesFlag, true), + Layer.succeed(TargetPolicy, { group: "hosted", consent: "no" }), + ); + +const confirmed = `Confirmed by --yes: Start a PITR restore without a recovery time target on project ${VALID_REF}\n`; + function setup(opts: SetupOpts = {}) { const out = mockOutput({ format: opts.format ?? "text" }); const api = mockCommandPlatformApi({ @@ -51,11 +65,11 @@ function setup(opts: SetupOpts = {}) { network: opts.network, }); const cliSettings = mockCommandSettings({ workdir: tempRoot.current }); - const layer = buildTestRuntime({ + const layer = buildRuntime({ out, api, cliSettings, - goOutput: opts.goOutput === undefined ? Option.none() : Option.some(opts.goOutput), + outputFlag: opts.outputFlag === undefined ? Option.none() : Option.some(opts.outputFlag), }); return { layer, out, api }; } @@ -65,6 +79,7 @@ describe("backups restore integration", () => { const { layer, api } = setup(); return Effect.gen(function* () { yield* backupsRestore({ + remote: Option.none(), projectRef: Option.none(), timestamp: Option.none(), }); @@ -78,6 +93,7 @@ describe("backups restore integration", () => { const { layer, api } = setup(); return Effect.gen(function* () { yield* backupsRestore({ + remote: Option.none(), projectRef: Option.none(), timestamp: Option.some(1_707_407_047), }); @@ -91,14 +107,17 @@ describe("backups restore integration", () => { }).pipe(Effect.provide(layer)); }); - it.effect("writes 'Started PITR restore: \\n' to stderr in text mode (Go parity)", () => { + it.effect("writes 'Started PITR restore: \\n' to stderr in text mode", () => { const { layer, out } = setup(); return Effect.gen(function* () { yield* backupsRestore({ + remote: Option.none(), projectRef: Option.none(), timestamp: Option.none(), }); - expect(out.stderrText).toBe(`Started PITR restore: ${VALID_REF}\n`); + expect(out.stderrText).toBe( + `${unverifiedTargetBanner()}${confirmed}Started PITR restore: ${VALID_REF}\n`, + ); expect(out.stdoutText).toBe(""); }).pipe(Effect.provide(layer)); }); @@ -107,6 +126,7 @@ describe("backups restore integration", () => { const { layer, out } = setup({ format: "json" }); return Effect.gen(function* () { yield* backupsRestore({ + remote: Option.none(), projectRef: Option.none(), timestamp: Option.none(), }); @@ -120,6 +140,7 @@ describe("backups restore integration", () => { const { layer, out } = setup({ format: "stream-json" }); return Effect.gen(function* () { yield* backupsRestore({ + remote: Option.none(), projectRef: Option.none(), timestamp: Option.none(), }); @@ -128,10 +149,11 @@ describe("backups restore integration", () => { }).pipe(Effect.provide(layer)); }); - it.effect("emits indented JSON to stdout for --output json (Go-compat)", () => { - const { layer, out } = setup({ goOutput: "json" }); + it.effect("emits indented JSON to stdout for --output json", () => { + const { layer, out } = setup({ outputFlag: "json" }); return Effect.gen(function* () { yield* backupsRestore({ + remote: Option.none(), projectRef: Option.none(), timestamp: Option.none(), }); @@ -142,32 +164,36 @@ describe("backups restore integration", () => { }); it.effect( - "renders the stderr text line for --output {pretty,yaml,toml,env} (Go ignores --output)", + "renders the stderr text line for --output {pretty,yaml,toml,env} (--output is ignored)", () => { - const { layer, out } = setup({ goOutput: "yaml" }); + const { layer, out } = setup({ outputFlag: "yaml" }); return Effect.gen(function* () { yield* backupsRestore({ + remote: Option.none(), projectRef: Option.none(), timestamp: Option.none(), }); - expect(out.stderrText).toBe(`Started PITR restore: ${VALID_REF}\n`); + expect(out.stderrText).toBe( + `${unverifiedTargetBanner()}${confirmed}Started PITR restore: ${VALID_REF}\n`, + ); expect(out.stdoutText).toBe(""); }).pipe(Effect.provide(layer)); }, ); - it.effect("uses --project-ref flag over CommandSettings.projectId", () => { + it.effect("uses the --remote= value over CommandSettings.projectId", () => { const flagRef = "zzzzzzzzzzzzzzzzzzzz"; const { layer, api } = setup(); return Effect.gen(function* () { yield* backupsRestore({ - projectRef: Option.some(flagRef), + remote: Option.some({ _tag: "Ref", ref: flagRef }), + projectRef: Option.none(), timestamp: Option.none(), }); expect(api.requests[0]?.url).toContain(`/v1/projects/${flagRef}/`); assertPermissionDrift({ command: "backups restore", - activeFlags: { "project-ref": true }, + activeFlags: { remote: flagRef }, requests: api.requests, exact: true, }); @@ -178,7 +204,11 @@ describe("backups restore integration", () => { const { layer } = setup({ status: 503 }); return Effect.gen(function* () { const exit = yield* Effect.exit( - backupsRestore({ projectRef: Option.none(), timestamp: Option.none() }), + backupsRestore({ + remote: Option.none(), + projectRef: Option.none(), + timestamp: Option.none(), + }), ); expect(Exit.isFailure(exit)).toBe(true); if (Exit.isFailure(exit)) { @@ -193,7 +223,11 @@ describe("backups restore integration", () => { const { layer } = setup({ network: "fail" }); return Effect.gen(function* () { const exit = yield* Effect.exit( - backupsRestore({ projectRef: Option.none(), timestamp: Option.none() }), + backupsRestore({ + remote: Option.none(), + projectRef: Option.none(), + timestamp: Option.none(), + }), ); expect(Exit.isFailure(exit)).toBe(true); if (Exit.isFailure(exit)) { @@ -211,13 +245,15 @@ describe("backups restore integration", () => { workdir: tempRoot.current, projectId: Option.none(), }); - const layer = buildTestRuntime({ out, api, cliSettings }); + const layer = buildRuntime({ out, api, cliSettings }); return Effect.gen(function* () { const exit = yield* Effect.exit( - backupsRestore({ projectRef: Option.none(), timestamp: Option.none() }).pipe( - Effect.provide(layer), - ), + backupsRestore({ + remote: Option.none(), + projectRef: Option.none(), + timestamp: Option.none(), + }).pipe(Effect.provide(layer)), ); expect(Exit.isFailure(exit)).toBe(true); if (Exit.isFailure(exit)) { @@ -251,7 +287,7 @@ describe("backups restore integration", () => { workdir: tempRoot.current, projectId: Option.none(), }); - const layer = buildTestRuntime({ + const layer = buildRuntime({ out, api, cliSettings, @@ -266,7 +302,11 @@ describe("backups restore integration", () => { const fs = yield* FileSystem.FileSystem; const cachePath = `${tempRoot.current}/supabase/.temp/linked-project.json`; expect(yield* fs.exists(cachePath)).toBe(false); - yield* backupsRestore({ projectRef: Option.none(), timestamp: Option.none() }); + yield* backupsRestore({ + remote: Option.none(), + projectRef: Option.none(), + timestamp: Option.none(), + }); expect(out.promptSelectCalls).toHaveLength(1); expect(out.stderrText).toContain(`Started PITR restore: ${VALID_REF}\n`); expect(api.requests.map(({ method, url }) => `${method} ${url}`)).toEqual([ @@ -286,6 +326,7 @@ describe("backups restore integration", () => { const { layer, api } = setup(); return Effect.gen(function* () { yield* backupsRestore({ + remote: Option.none(), projectRef: Option.none(), timestamp: Option.some(42), }); @@ -293,3 +334,19 @@ describe("backups restore integration", () => { }).pipe(Effect.provide(layer)); }); }); + +describeHostedConsent("backups restore", { + api: { response: { status: 201, body: null } }, + invoke: () => ({ + run: backupsRestore({ + remote: Option.none(), + projectRef: Option.none(), + timestamp: Option.some(1_707_407_047), + }), + }), +}); + +describeHostedTargetSelection("backups restore", { + respond: (request) => Effect.succeed(jsonResponse(request, 201, null)), + run: (flags) => backupsRestore({ ...flags, timestamp: Option.none() }), +}); diff --git a/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md b/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md index 5702c4f148..3dfd263281 100644 --- a/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md @@ -1,38 +1,40 @@ # `supabase bootstrap [template]` -`bootstrap` is a meta-orchestrator: it chains a workdir prompt → template fetch/download → -blank `init` → ensure-login → `projects create` → `projects api-keys` → `link` services → +Target: account, consent default Yes — see [Command target](../../../docs/command-target.md) + +`bootstrap` is a meta-orchestrator: it chains a workdir prompt → hosted-mutation consent → +template fetch/download → blank `init` → ensure-login → `projects create` → `projects api-keys` → `link` services → health poll → write `.env` → `db push` → start suggestion. Every step is native TypeScript, including the migration push (`dbPushCore`, shared with the standalone `supabase db push` command — see Notes). ## Files Read -| Path | Format | When | -| ------------------------------------------------------------------ | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `~/.supabase/access-token` | plain text | ensure-login token miss (env unset and keyring unavailable) | -| `/.env.example` | dotenv | optional; merged into the generated `.env` | -| `/supabase/{.env..local,.env.local,.env.,.env}` | dotenv | step I (`loadProjectEnv`), before config.toml validation and again inside `checkDbToml`; `` is `SUPABASE_ENV` (default `development`), `.env.local` is skipped when `SUPABASE_ENV=test`; first of the 4 files (in this order) to set a key wins, and this `supabase/` directory tier beats the workdir-root tier below — feeds config.toml `env(VAR)` expansion and the push step's `SUPABASE_YES` auto-confirm default | -| `/{.env..local,.env.local,.env.,.env}` | dotenv | same read as above; lower-precedence fallback tier, only consulted for a key none of the `supabase/` directory's 4 files above already set | -| `/supabase/config.toml` | TOML | native push step (embedded defaults used when absent) | -| `/supabase/.temp/pooler-url` | plain text | native push step's connection resolution, only when the direct `db..:5432` host is unreachable (IPv4-only network) — `resolveLinkedConn` falls back through the saved pooler URL `link.LinkServices` wrote in the earlier link-services step | -| `/supabase/migrations/` | directory | native push step, when `[db.migrations].enabled` (default true) | -| `/supabase/migrations/*.sql` | SQL | native push step, for each pending migration applied | -| seed files from `[db.seed].sql_paths` | SQL | native push step (`--include-seed` is always set; gated on `[db.seed].enabled`) | -| `/supabase/roles.sql` | SQL | native push step (`--include-roles` is always set; existence check + apply) | -| `PGSSL*` PEM, `pg_service.conf` | PEM, libpq | native push step, linked project, when set; see `db push` | +| Path | Format | When | +| ------------------------------------------------------------------ | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `~/.supabase/access-token` | plain text | ensure-login token miss (env unset and keyring unavailable) | +| `/.env.example` | dotenv | optional; merged into the generated `.env` | +| `/supabase/{.env..local,.env.local,.env.,.env}` | dotenv | step I (`loadProjectEnvValues`), before config.toml validation and again inside `checkDbToml`; `` is `SUPABASE_ENV` (default `development`), `.env.local` is skipped when `SUPABASE_ENV=test`; first of the 4 files (in this order) to set a key wins, and this `supabase/` directory tier beats the workdir-root tier below — feeds config.toml `env(VAR)` expansion and the push step's `SUPABASE_YES` auto-confirm default | +| `/{.env..local,.env.local,.env.,.env}` | dotenv | same read as above; lower-precedence fallback tier, only consulted for a key none of the `supabase/` directory's 4 files above already set | +| `/supabase/config.toml` | TOML | native push step (embedded defaults used when absent) | +| `/supabase/.temp/pooler-url` | plain text | native push step's connection resolution, only when the direct `db..:5432` host is unreachable (IPv4-only network) — `resolveLinkedConn` falls back through the saved pooler URL `link.LinkServices` wrote in the earlier link-services step | +| `/supabase/migrations/` | directory | native push step, when `[db.migrations].enabled` (default true) | +| `/supabase/migrations/*.sql` | SQL | native push step, for each pending migration applied | +| seed files from `[db.seed].sql_paths` | SQL | native push step (`--include-seed` is always set; gated on `[db.seed].enabled`) | +| `/supabase/roles.sql` | SQL | native push step (`--include-roles` is always set; existence check + apply) | +| `PGSSL*` PEM, `pg_service.conf` | PEM, libpq | native push step, remote project, when set; see `db push` | ## Files Written -| Path | Format | When | -| ----------------------------------------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `/supabase/config.toml` | TOML | blank/`scratch` path only (via `initProject`) | -| `/