Compare commits
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
freebsd_instance:
|
freebsd_instance:
|
||||||
image_family: freebsd-14-2
|
image_family: freebsd-14-3
|
||||||
|
|
||||||
build_task:
|
build_task:
|
||||||
# Don't change this name without adjusting .github/workflows/build.yaml
|
# Don't change this name without adjusting .github/workflows/build.yaml
|
||||||
@@ -35,7 +35,7 @@ build_task:
|
|||||||
- find main src -type f -iname '*.hs' -exec md5sum "{}" +
|
- find main src -type f -iname '*.hs' -exec md5sum "{}" +
|
||||||
|
|
||||||
build_script: |
|
build_script: |
|
||||||
stack build -j 1 --local-bin-path . --copy-bins --stack-yaml stack-21.7.yaml
|
stack build -j 1 --local-bin-path . --copy-bins
|
||||||
strip postgrest
|
strip postgrest
|
||||||
|
|
||||||
bin_artifacts:
|
bin_artifacts:
|
||||||
|
|||||||
@@ -19,14 +19,14 @@ inputs:
|
|||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
- uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
if: ${{ startsWith(github.ref, 'refs/heads/') || (inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
if: ${{ startsWith(github.ref, 'refs/heads/') || (inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
||||||
with:
|
with:
|
||||||
path: ${{ inputs.path }}
|
path: ${{ inputs.path }}
|
||||||
key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
|
key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-${{ inputs.prefix }}-
|
${{ runner.os }}-${{ inputs.prefix }}-
|
||||||
- uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
- uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
if: ${{ !startsWith(github.ref, 'refs/heads/') && !(inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
if: ${{ !startsWith(github.ref, 'refs/heads/') && !(inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
||||||
with:
|
with:
|
||||||
path: ${{ inputs.path }}
|
path: ${{ inputs.path }}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ inputs:
|
|||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- uses: nixbuild/nix-quick-install-action@5bb6a3b3abe66fd09bbf250dce8ada94f856a703 # v30
|
- uses: nixbuild/nix-quick-install-action@1f095fee853b33114486cfdeae62fa099cda35a9 # v33
|
||||||
with:
|
with:
|
||||||
nix_conf: |-
|
nix_conf: |-
|
||||||
always-allow-substitutes = true
|
always-allow-substitutes = true
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
name: Nix - Linux x86-64 static
|
name: Nix - Linux x86-64 static
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
name: Nix - MacOS
|
name: Nix - MacOS
|
||||||
runs-on: macos-14
|
runs-on: macos-14
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
@@ -126,11 +126,11 @@ jobs:
|
|||||||
name: Stack - ${{ matrix.name }}
|
name: Stack - ${{ matrix.name }}
|
||||||
runs-on: ${{ matrix.runs-on }}
|
runs-on: ${{ matrix.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: haskell-actions/setup@64445b6b5dd545faf5f8e2acee8253eb5c2b29aa # v2.7.11
|
- uses: haskell-actions/setup@7909071ceec0344debcc968c6c7a96a52e8dd0d7 # v2.8.1
|
||||||
with:
|
with:
|
||||||
# This must match the version in stack.yaml's resolver
|
# This must match the version in stack.yaml's resolver
|
||||||
ghc-version: 9.6.6
|
ghc-version: 9.6.7
|
||||||
enable-stack: true
|
enable-stack: true
|
||||||
stack-no-global: true
|
stack-no-global: true
|
||||||
stack-setup-ghc: true
|
stack-setup-ghc: true
|
||||||
@@ -168,7 +168,7 @@ jobs:
|
|||||||
name: Stack - FreeBSD from CirrusCI
|
name: Stack - FreeBSD from CirrusCI
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: ./.github/actions/artifact-from-cirrus
|
- uses: ./.github/actions/artifact-from-cirrus
|
||||||
with:
|
with:
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
@@ -180,13 +180,13 @@ jobs:
|
|||||||
cabal:
|
cabal:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
ghc: ['9.6.6', '9.8.2']
|
ghc: ['9.6.7', '9.8.4']
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
name: Cabal - Linux x86-64 - GHC ${{ matrix.ghc }}
|
name: Cabal - Linux x86-64 - GHC ${{ matrix.ghc }}
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: haskell-actions/setup@64445b6b5dd545faf5f8e2acee8253eb5c2b29aa # v2.7.11
|
- uses: haskell-actions/setup@7909071ceec0344debcc968c6c7a96a52e8dd0d7 # v2.8.1
|
||||||
with:
|
with:
|
||||||
ghc-version: ${{ matrix.ghc }}
|
ghc-version: ${{ matrix.ghc }}
|
||||||
- name: Cache .cabal
|
- name: Cache .cabal
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
name: Lint & Style
|
name: Lint & Style
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
- test
|
- test
|
||||||
- build
|
- build
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
ssh-key: ${{ secrets.POSTGREST_SSH_KEY }}
|
ssh-key: ${{ secrets.POSTGREST_SSH_KEY }}
|
||||||
- name: Tag latest commit
|
- name: Tag latest commit
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
name: Build
|
name: Build
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
name: Spellcheck
|
name: Spellcheck
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ jobs:
|
|||||||
linkcheck:
|
linkcheck:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- build
|
- build
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Check the version to be released
|
- name: Check the version to be released
|
||||||
run: |
|
run: |
|
||||||
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
|
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
|
||||||
@@ -64,9 +64,9 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- prepare
|
- prepare
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
- name: Create release bundle with archives for all builds
|
- name: Create release bundle with archives for all builds
|
||||||
@@ -138,17 +138,17 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
DOCKER_REPO: ${{ vars.DOCKER_REPO }}
|
DOCKER_REPO: ${{ vars.DOCKER_REPO }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Download x86-64 Docker image
|
- name: Download x86-64 Docker image
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: postgrest-docker-x86-64
|
name: postgrest-docker-x86-64
|
||||||
- name: Download aarch64 binary
|
- name: Download aarch64 binary
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: postgrest-ubuntu-aarch64
|
name: postgrest-ubuntu-aarch64
|
||||||
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
- uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
username: ${{ vars.DOCKER_USER }}
|
username: ${{ vars.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_PASS }}
|
password: ${{ secrets.DOCKER_PASS }}
|
||||||
@@ -194,7 +194,7 @@ jobs:
|
|||||||
vars.DOCKER_REPO && vars.DOCKER_USER &&
|
vars.DOCKER_REPO && vars.DOCKER_USER &&
|
||||||
github.ref == 'refs/tags/devel'
|
github.ref == 'refs/tags/devel'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4.0.2
|
- uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4.0.2
|
||||||
with:
|
with:
|
||||||
username: ${{ vars.DOCKER_USER }}
|
username: ${{ vars.DOCKER_USER }}
|
||||||
|
|||||||
+14
-11
@@ -39,7 +39,7 @@ jobs:
|
|||||||
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
||||||
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
|
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
|
||||||
run: postgrest-coverage
|
run: postgrest-coverage
|
||||||
- name: Upload coverage to codecov
|
- name: Upload coverage to codecov
|
||||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||||
with:
|
with:
|
||||||
files: ./coverage/codecov.json
|
files: ./coverage/codecov.json
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
||||||
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
name: Memory
|
name: Memory
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
@@ -117,7 +117,7 @@ jobs:
|
|||||||
name: Loadtest
|
name: Loadtest
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
@@ -125,13 +125,16 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
tools: loadtest.loadtestAgainst.bin loadtest.report.bin
|
tools: loadtest.loadtestAgainst.bin loadtest.report.bin
|
||||||
- uses: WyriHaximus/github-action-get-previous-tag@04e8485ecb6487243907e330d522ff60f02283ce # v1.4.0
|
|
||||||
id: get-latest-tag
|
|
||||||
with:
|
|
||||||
prefix: v
|
|
||||||
- name: Run loadtest
|
- name: Run loadtest
|
||||||
|
env:
|
||||||
|
TARGET_BRANCH: ${{ github.base_ref || github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
postgrest-loadtest-against -k ${{ matrix.kind }} main ${{ steps.get-latest-tag.outputs.tag }}
|
if [ "$TARGET_BRANCH" = "main" ]; then
|
||||||
|
latest_tag=$(git tag --sort=-creatordate --list "v*" | head -n1)
|
||||||
|
else
|
||||||
|
latest_tag=$(git tag --merged HEAD --sort=-creatordate "v*" | head -n1)
|
||||||
|
fi
|
||||||
|
postgrest-loadtest-against -k ${{ matrix.kind }} "$TARGET_BRANCH" "$latest_tag"
|
||||||
postgrest-loadtest-report >> "$GITHUB_STEP_SUMMARY"
|
postgrest-loadtest-report >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
flake:
|
flake:
|
||||||
@@ -146,7 +149,7 @@ jobs:
|
|||||||
name: Flake Check
|
name: Flake Check
|
||||||
runs-on: ${{ matrix.runs-on }}
|
runs-on: ${{ matrix.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
|
|||||||
+9
-9
@@ -18,16 +18,16 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
<img width="296px" src="static/code-build.png">
|
<img width="296px" src="static/tembo.png">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr></tr>
|
<tr></tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
||||||
<img width="296px" src="static/tembo.png">
|
<img width="296px" src="static/euronodes.svg">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
@@ -35,11 +35,6 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
|||||||
<img width="296px" src="static/supabase.png">
|
<img width="296px" src="static/supabase.png">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
|
||||||
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
|
||||||
<img width="296px" src="static/euronodes.svg">
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -93,6 +88,11 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
|||||||
<img width="222px" src="static/oblivious.jpg">
|
<img width="222px" src="static/oblivious.jpg">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" valign="middle">
|
||||||
|
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
|
<img width="222px" src="static/code-build.png">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -5,6 +5,39 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## [13.0.7] - 2025-09-14
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Improve the `PGRST106` error when the requested schema is invalid by @laurenceisla in #4089
|
||||||
|
+ It now shows the invalid schema in the `message` field.
|
||||||
|
+ The exposed schemas are now listed in the `hint` instead of the `message` field.
|
||||||
|
- Improve error details of `PGRST301` error by @taimoorzaeem in #4051
|
||||||
|
|
||||||
|
## [13.0.6] - 2025-08-30
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix logging the Haskell type instead of the listener error message directly by @laurenceisla in #3588
|
||||||
|
- Fix format of `IPv6` address logged at PostgREST startup by @taimoorzaeem in #4291
|
||||||
|
- Fix empty enum in `preferParams` OpenAPI parameter by @laurenceisla in #4292
|
||||||
|
|
||||||
|
## [13.0.5] - 2025-08-24
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix OpenAPI broken docs link by @taimoorzaeem in #4048
|
||||||
|
- Fix OpenAPI specification incorrectly exposing GET methods for volatile functions by @joelonsql in #4174
|
||||||
|
- Fix empty spread embeddings return unexpected SQL error by @taimoorzaeem in #3887
|
||||||
|
- Fix `/metrics` endpoint not responding with `Content-Type` header by @taimoorzaeem in #4271
|
||||||
|
|
||||||
|
## [13.0.4] - 2025-06-17
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix regression that makes full-text search not work on domain types based on `tsvector` by @laurenceisla in #4135
|
||||||
|
- Fix `jwt-aud` config not failing when set to an invalid URI by @taimoorzaeem in #4132
|
||||||
|
|
||||||
## [13.0.3] - 2025-06-16
|
## [13.0.3] - 2025-06-16
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
# The x86-64 is a single-static-binary image built via Nix, see:
|
# The x86-64 is a single-static-binary image built via Nix, see:
|
||||||
# nix/tools/docker/README.md
|
# nix/tools/docker/README.md
|
||||||
|
|
||||||
FROM ubuntu:noble@sha256:6015f66923d7afbc53558d7ccffd325d43b4e249f41a6e93eef074c9505d2233 AS postgrest
|
FROM ubuntu:noble@sha256:9cbed754112939e914291337b5e554b07ad7c392491dba6daf25eef1332a22e8 AS postgrest
|
||||||
|
|
||||||
RUN apt-get update -y \
|
RUN apt-get update -y \
|
||||||
&& apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \
|
&& apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \
|
||||||
|
|||||||
@@ -27,16 +27,16 @@ API than you are likely to write from scratch.
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
<img width="296px" src="static/code-build.png">
|
<img width="296px" src="static/tembo.png">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr></tr>
|
<tr></tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
||||||
<img width="296px" src="static/tembo.png">
|
<img width="296px" src="static/euronodes.svg">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
@@ -44,11 +44,6 @@ API than you are likely to write from scratch.
|
|||||||
<img width="296px" src="static/supabase.png">
|
<img width="296px" src="static/supabase.png">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
|
||||||
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
|
||||||
<img width="296px" src="static/euronodes.svg">
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
index-state: hackage.haskell.org 2025-02-01T14:59:33Z
|
index-state: hackage.haskell.org 2025-07-08T15:50:10Z
|
||||||
|
|||||||
@@ -24,16 +24,16 @@ write from scratch.
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/code-build.png">
|
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/tembo.png">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr></tr>
|
<tr></tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
||||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/tembo.png">
|
<img width="296px" src="static/euronodes.svg">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
@@ -41,11 +41,6 @@ write from scratch.
|
|||||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.png">
|
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.png">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
|
||||||
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
|
||||||
<img width="296px" src="static/euronodes.svg">
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
+15
-25
@@ -41,28 +41,28 @@ Sponsors
|
|||||||
.. container:: img-dark
|
.. container:: img-dark
|
||||||
|
|
||||||
.. image:: ../static/neon-dark.jpg
|
.. image:: ../static/neon-dark.jpg
|
||||||
:target: https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest
|
:target: https://neon.com/?utm_source=sponsor&utm_campaign=postgrest
|
||||||
|
|
||||||
.. container:: img-light
|
.. container:: img-light
|
||||||
|
|
||||||
.. image:: ../static/neon.jpg
|
.. image:: ../static/neon.jpg
|
||||||
:target: https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest
|
:target: https://neon.com/?utm_source=sponsor&utm_campaign=postgrest
|
||||||
|
|
||||||
.. container:: img-dark
|
|
||||||
|
|
||||||
.. image:: ../static/code-build-dark.png
|
|
||||||
:target: https://code.build/?utm_source=sponsor&utm_campaign=postgrest
|
|
||||||
|
|
||||||
.. container:: img-light
|
|
||||||
|
|
||||||
.. image:: ../static/code-build.png
|
|
||||||
:target: https://code.build/?utm_source=sponsor&utm_campaign=postgrest
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
||||||
.. image:: ../static/tembo.png
|
.. image:: ../static/tembo.png
|
||||||
:target: https://www.tembo.io/?utm_source=sponsor&utm_campaign=postgrest
|
:target: https://www.tembo.io/?utm_source=sponsor&utm_campaign=postgrest
|
||||||
|
|
||||||
|
|
|
||||||
|
|
||||||
|
.. container:: img-dark
|
||||||
|
|
||||||
|
.. image:: ../static/euronodes.svg
|
||||||
|
:target: https://www.euronodes.com/postgrest
|
||||||
|
|
||||||
|
.. container:: img-light
|
||||||
|
|
||||||
|
.. image:: ../static/euronodes.svg
|
||||||
|
:target: https://www.euronodes.com/postgrest
|
||||||
|
|
||||||
.. container:: img-dark
|
.. container:: img-dark
|
||||||
|
|
||||||
.. image:: ../static/supabase-dark.png
|
.. image:: ../static/supabase-dark.png
|
||||||
@@ -73,20 +73,10 @@ Sponsors
|
|||||||
.. image:: ../static/supabase.png
|
.. image:: ../static/supabase.png
|
||||||
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
|
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
|
||||||
|
|
||||||
.. container:: img-dark
|
|
||||||
|
|
||||||
.. image:: ../static/euronodes.svg
|
|
||||||
:target: https://www.euronodes.com/postgrest
|
|
||||||
|
|
||||||
.. container:: img-light
|
|
||||||
|
|
||||||
.. image:: ../static/euronodes.svg
|
|
||||||
:target: https://www.euronodes.com/postgrest
|
|
||||||
|
|
||||||
.. The static/empty.png(created with `convert -size 320x95 xc:#fcfcfc empty.png`) is an ugly workaround
|
.. The static/empty.png(created with `convert -size 320x95 xc:#fcfcfc empty.png`) is an ugly workaround
|
||||||
to create space and center the logos. It's not easy to layout with restructuredText.
|
to create space and center the logos. It's not easy to layout with restructuredText.
|
||||||
|
|
||||||
.. .. image:: _static/empty.png
|
.. image:: _static/empty.png
|
||||||
:target: #sponsors
|
:target: #sponsors
|
||||||
|
|
||||||
|
|
|
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ unikernel
|
|||||||
unix
|
unix
|
||||||
updatable
|
updatable
|
||||||
unfulfillable
|
unfulfillable
|
||||||
|
unselected
|
||||||
Untyped
|
Untyped
|
||||||
UPSERT
|
UPSERT
|
||||||
Upsert
|
Upsert
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Health Check
|
|||||||
|
|
||||||
You can enable a health check to verify if PostgREST is available for client requests. Also to check the status of its internal state.
|
You can enable a health check to verify if PostgREST is available for client requests. Also to check the status of its internal state.
|
||||||
|
|
||||||
Two endpoints ``live`` and ``ready`` will then be available.
|
Two endpoints ``live`` and ``ready`` will then be available. Both these endpoints reply with a status code and empty response body.
|
||||||
|
|
||||||
.. important::
|
.. important::
|
||||||
|
|
||||||
|
|||||||
@@ -294,6 +294,23 @@ Let's get its :ref:`explain_plan` when calling it with filters applied:
|
|||||||
|
|
||||||
Notice there's no "Function Scan" node in the plan, which tells us it has been inlined.
|
Notice there's no "Function Scan" node in the plan, which tells us it has been inlined.
|
||||||
|
|
||||||
|
Horizontal Filtering
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Table-valued functions support horizontal filtering on selected and unselected columns.
|
||||||
|
|
||||||
|
For example, the following RPC with filter on unselected column returns:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
curl "http://localhost:3000/rpc/getallprojects?select=id,client_id&name=like.OSX"
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
[
|
||||||
|
{ "id": 4, "client_id": 2 }
|
||||||
|
]
|
||||||
|
|
||||||
.. _scalar_functions:
|
.. _scalar_functions:
|
||||||
|
|
||||||
Scalar functions
|
Scalar functions
|
||||||
|
|||||||
@@ -128,6 +128,11 @@ The ``metrics`` endpoint on the :ref:`admin_server` endpoint provides metrics in
|
|||||||
|
|
||||||
curl "http://localhost:3001/metrics"
|
curl "http://localhost:3001/metrics"
|
||||||
|
|
||||||
|
.. code-block:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: text/plain; charset=utf-8
|
||||||
|
|
||||||
# HELP pgrst_schema_cache_query_time_seconds The query time in seconds of the last schema cache load
|
# HELP pgrst_schema_cache_query_time_seconds The query time in seconds of the last schema cache load
|
||||||
# TYPE pgrst_schema_cache_query_time_seconds gauge
|
# TYPE pgrst_schema_cache_query_time_seconds gauge
|
||||||
pgrst_schema_cache_query_time_seconds 1.5937927e-2
|
pgrst_schema_cache_query_time_seconds 1.5937927e-2
|
||||||
@@ -246,7 +251,7 @@ See :ref:`proxy-status_header`.
|
|||||||
Server-Timing Header
|
Server-Timing Header
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
You can enable the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing>`_ header by setting :ref:`server-timing-enabled` on.
|
You can enable the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing>`_ header by setting :ref:`server-timing-enabled` on.
|
||||||
This header communicates metrics of the different phases in the request-response cycle.
|
This header communicates metrics of the different phases in the request-response cycle.
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|||||||
+14
-17
@@ -1,5 +1,5 @@
|
|||||||
name: postgrest
|
name: postgrest
|
||||||
version: 13.0.3
|
version: 13.0.7
|
||||||
synopsis: REST API for any Postgres database
|
synopsis: REST API for any Postgres database
|
||||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||||
for tables, views, and functions, supporting all HTTP methods that security
|
for tables, views, and functions, supporting all HTTP methods that security
|
||||||
@@ -16,15 +16,13 @@ extra-source-files: CHANGELOG.md
|
|||||||
cabal-version: >= 1.10
|
cabal-version: >= 1.10
|
||||||
|
|
||||||
tested-with:
|
tested-with:
|
||||||
-- stack on FreeBSD
|
-- nix
|
||||||
GHC == 9.4.5
|
GHC == 9.4.8
|
||||||
-- nix, cabal on Ubuntu (arm)
|
|
||||||
, GHC == 9.4.8
|
|
||||||
-- cabal on Ubuntu
|
-- cabal on Ubuntu
|
||||||
-- stack on MacOS, Ubuntu, Windows
|
-- stack on FreeBSD, MacOS, Ubuntu, Windows
|
||||||
, GHC == 9.6.6
|
, GHC == 9.6.7
|
||||||
-- cabal on Ubuntu
|
-- cabal on Ubuntu
|
||||||
, GHC == 9.8.2
|
, GHC == 9.8.4
|
||||||
|
|
||||||
source-repository head
|
source-repository head
|
||||||
type: git
|
type: git
|
||||||
@@ -91,12 +89,11 @@ library
|
|||||||
PostgREST.Response.GucHeader
|
PostgREST.Response.GucHeader
|
||||||
PostgREST.Response.Performance
|
PostgREST.Response.Performance
|
||||||
PostgREST.Version
|
PostgREST.Version
|
||||||
other-modules: Paths_postgrest
|
|
||||||
build-depends: base >= 4.9 && < 4.20
|
build-depends: base >= 4.9 && < 4.20
|
||||||
, HTTP >= 4000.3.7 && < 4000.5
|
, HTTP >= 4000.3.7 && < 4000.5
|
||||||
, Ranged-sets >= 0.3 && < 0.5
|
, Ranged-sets >= 0.3 && < 0.5
|
||||||
, aeson >= 2.0.3 && < 2.3
|
, aeson >= 2.0.3 && < 2.3
|
||||||
, auto-update >= 0.1.4 && < 0.2
|
, auto-update >= 0.1.4 && < 0.3
|
||||||
, base64-bytestring >= 1 && < 1.3
|
, base64-bytestring >= 1 && < 1.3
|
||||||
, bytestring >= 0.10.8 && < 0.13
|
, bytestring >= 0.10.8 && < 0.13
|
||||||
, cache >= 0.1.3 && < 0.2.0
|
, cache >= 0.1.3 && < 0.2.0
|
||||||
@@ -105,7 +102,7 @@ library
|
|||||||
, clock >= 0.8.3 && < 0.9.0
|
, clock >= 0.8.3 && < 0.9.0
|
||||||
, configurator-pg >= 0.2 && < 0.3
|
, configurator-pg >= 0.2 && < 0.3
|
||||||
, containers >= 0.5.7 && < 0.7
|
, containers >= 0.5.7 && < 0.7
|
||||||
, cookie >= 0.4.2 && < 0.5
|
, cookie >= 0.4.2 && < 0.6
|
||||||
, directory >= 1.2.6 && < 1.4
|
, directory >= 1.2.6 && < 1.4
|
||||||
, either >= 4.4.1 && < 5.1
|
, either >= 4.4.1 && < 5.1
|
||||||
, extra >= 1.7.0 && < 2.0
|
, extra >= 1.7.0 && < 2.0
|
||||||
@@ -114,17 +111,17 @@ library
|
|||||||
, hasql-dynamic-statements >= 0.3.1 && < 0.4
|
, hasql-dynamic-statements >= 0.3.1 && < 0.4
|
||||||
, hasql-notifications >= 0.2.2.2 && < 0.2.3
|
, hasql-notifications >= 0.2.2.2 && < 0.2.3
|
||||||
, hasql-pool >= 1.0.1 && < 1.1
|
, hasql-pool >= 1.0.1 && < 1.1
|
||||||
, hasql-transaction >= 1.0.1 && < 1.1
|
, hasql-transaction >= 1.0.1 && < 1.2
|
||||||
, heredoc >= 0.2 && < 0.3
|
, heredoc >= 0.2 && < 0.3
|
||||||
, http-types >= 0.12.2 && < 0.13
|
, http-types >= 0.12.2 && < 0.13
|
||||||
, insert-ordered-containers >= 0.2.2 && < 0.3
|
, insert-ordered-containers >= 0.2.2 && < 0.3
|
||||||
, iproute >= 1.7.0 && < 1.8
|
, iproute >= 1.7.0 && < 1.8
|
||||||
, jose-jwt >= 0.9.6 && < 0.11
|
, jose-jwt >= 0.9.6 && < 0.11
|
||||||
, lens >= 4.14 && < 5.3
|
, lens >= 4.14 && < 5.4
|
||||||
, lens-aeson >= 1.0.1 && < 1.3
|
, lens-aeson >= 1.0.1 && < 1.3
|
||||||
, mtl >= 2.2.2 && < 2.4
|
, mtl >= 2.2.2 && < 2.4
|
||||||
, neat-interpolation >= 0.5 && < 0.6
|
, neat-interpolation >= 0.5 && < 0.6
|
||||||
, network >= 2.6 && < 3.2
|
, network >= 2.6 && < 3.3
|
||||||
, network-uri >= 2.6.1 && < 2.8
|
, network-uri >= 2.6.1 && < 2.8
|
||||||
, optparse-applicative >= 0.13 && < 0.19
|
, optparse-applicative >= 0.13 && < 0.19
|
||||||
, parsec >= 3.1.11 && < 3.2
|
, parsec >= 3.1.11 && < 3.2
|
||||||
@@ -152,7 +149,7 @@ library
|
|||||||
-- for unix sockets; this is tested in test/io/test_io.py. See
|
-- for unix sockets; this is tested in test/io/test_io.py. See
|
||||||
-- https://github.com/kazu-yamamoto/logger/commit/3a71ca70afdbb93d4ecf0083eeba1fbbbcab3fc3
|
-- https://github.com/kazu-yamamoto/logger/commit/3a71ca70afdbb93d4ecf0083eeba1fbbbcab3fc3
|
||||||
, wai-logger >= 2.4.0
|
, wai-logger >= 2.4.0
|
||||||
, warp >= 3.3.19 && < 3.4
|
, warp >= 3.3.19 && < 3.5
|
||||||
-- -fno-spec-constr may help keep compile time memory use in check,
|
-- -fno-spec-constr may help keep compile time memory use in check,
|
||||||
-- see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304
|
-- see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304
|
||||||
-- -optP-Wno-nonportable-include-path
|
-- -optP-Wno-nonportable-include-path
|
||||||
@@ -261,14 +258,14 @@ test-suite spec
|
|||||||
, case-insensitive >= 1.2 && < 1.3
|
, case-insensitive >= 1.2 && < 1.3
|
||||||
, containers >= 0.5.7 && < 0.7
|
, containers >= 0.5.7 && < 0.7
|
||||||
, hasql-pool >= 1.0.1 && < 1.1
|
, hasql-pool >= 1.0.1 && < 1.1
|
||||||
, hasql-transaction >= 1.0.1 && < 1.1
|
, hasql-transaction >= 1.0.1 && < 1.2
|
||||||
, heredoc >= 0.2 && < 0.3
|
, heredoc >= 0.2 && < 0.3
|
||||||
, hspec >= 2.3 && < 2.12
|
, hspec >= 2.3 && < 2.12
|
||||||
, hspec-wai >= 0.10 && < 0.12
|
, hspec-wai >= 0.10 && < 0.12
|
||||||
, hspec-wai-json >= 0.10 && < 0.12
|
, hspec-wai-json >= 0.10 && < 0.12
|
||||||
, http-types >= 0.12.3 && < 0.13
|
, http-types >= 0.12.3 && < 0.13
|
||||||
, jose-jwt >= 0.9.6 && < 0.11
|
, jose-jwt >= 0.9.6 && < 0.11
|
||||||
, lens >= 4.14 && < 5.3
|
, lens >= 4.14 && < 5.4
|
||||||
, lens-aeson >= 1.0.1 && < 1.3
|
, lens-aeson >= 1.0.1 && < 1.3
|
||||||
, monad-control >= 1.0.1 && < 1.1
|
, monad-control >= 1.0.1 && < 1.1
|
||||||
, postgrest
|
, postgrest
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import Network.Socket.ByteString
|
|||||||
|
|
||||||
import PostgREST.AppState (AppState)
|
import PostgREST.AppState (AppState)
|
||||||
import PostgREST.Config (AppConfig (..))
|
import PostgREST.Config (AppConfig (..))
|
||||||
|
import PostgREST.MediaType (MediaType (..), toContentType)
|
||||||
import PostgREST.Metrics (metricsToText)
|
import PostgREST.Metrics (metricsToText)
|
||||||
import PostgREST.Network (resolveHost)
|
import PostgREST.Network (resolveHost)
|
||||||
import PostgREST.Observation (Observation (..))
|
import PostgREST.Observation (Observation (..))
|
||||||
@@ -58,7 +59,7 @@ admin appState req respond = do
|
|||||||
respond $ Wai.responseLBS HTTP.status200 [] (maybe mempty JSON.encode sCache)
|
respond $ Wai.responseLBS HTTP.status200 [] (maybe mempty JSON.encode sCache)
|
||||||
["metrics"] -> do
|
["metrics"] -> do
|
||||||
mets <- metricsToText
|
mets <- metricsToText
|
||||||
respond $ Wai.responseLBS HTTP.status200 [] mets
|
respond $ Wai.responseLBS HTTP.status200 [toContentType MTTextPlain] mets -- Content-Type is required for prometheus compliance
|
||||||
_ ->
|
_ ->
|
||||||
respond $ Wai.responseLBS HTTP.status404 [] mempty
|
respond $ Wai.responseLBS HTTP.status404 [] mempty
|
||||||
|
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ getAction resource schema method =
|
|||||||
getSchema :: AppConfig -> RequestHeaders -> ByteString -> Either ApiRequestError (Schema, Bool)
|
getSchema :: AppConfig -> RequestHeaders -> ByteString -> Either ApiRequestError (Schema, Bool)
|
||||||
getSchema AppConfig{configDbSchemas} hdrs method = do
|
getSchema AppConfig{configDbSchemas} hdrs method = do
|
||||||
case profile of
|
case profile of
|
||||||
Just p | p `notElem` configDbSchemas -> Left $ UnacceptableSchema $ toList configDbSchemas
|
Just p | p `notElem` configDbSchemas -> Left $ UnacceptableSchema p $ toList configDbSchemas
|
||||||
| otherwise -> Right (p, True)
|
| otherwise -> Right (p, True)
|
||||||
Nothing -> Right (defaultSchema, length configDbSchemas /= 1) -- if we have many schemas, assume the default schema was negotiated
|
Nothing -> Right (defaultSchema, length configDbSchemas /= 1) -- if we have many schemas, assume the default schema was negotiated
|
||||||
where
|
where
|
||||||
|
|||||||
+19
-18
@@ -50,7 +50,8 @@ import PostgREST.Auth.JwtCache (lookupJwtCache)
|
|||||||
import PostgREST.Auth.Types (AuthResult (..))
|
import PostgREST.Auth.Types (AuthResult (..))
|
||||||
import PostgREST.Config (AppConfig (..), FilterExp (..),
|
import PostgREST.Config (AppConfig (..), FilterExp (..),
|
||||||
JSPath, JSPathExp (..))
|
JSPath, JSPathExp (..))
|
||||||
import PostgREST.Error (Error (..), JwtError (..))
|
import PostgREST.Error (Error (..), JwtClaimsError (..),
|
||||||
|
JwtDecodeError (..), JwtError (..))
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ import Protolude
|
|||||||
-- JSON object of JWT claims.
|
-- JSON object of JWT claims.
|
||||||
parseToken :: AppConfig -> Maybe ByteString -> UTCTime -> ExceptT Error IO JSON.Value
|
parseToken :: AppConfig -> Maybe ByteString -> UTCTime -> ExceptT Error IO JSON.Value
|
||||||
parseToken _ Nothing _ = return JSON.emptyObject
|
parseToken _ Nothing _ = return JSON.emptyObject
|
||||||
parseToken _ (Just "") _ = throwE . JwtErr $ JwtDecodeError "Empty JWT is sent in Authorization header"
|
parseToken _ (Just "") _ = throwE . JwtErr $ JwtDecodeErr EmptyAuthHeader
|
||||||
parseToken AppConfig{..} (Just tkn) time = do
|
parseToken AppConfig{..} (Just tkn) time = do
|
||||||
secret <- liftEither . maybeToRight (JwtErr JwtSecretMissing) $ configJWKS
|
secret <- liftEither . maybeToRight (JwtErr JwtSecretMissing) $ configJWKS
|
||||||
tknWith3Parts <- liftEither $ hasThreeParts tkn
|
tknWith3Parts <- liftEither $ hasThreeParts tkn
|
||||||
@@ -69,33 +70,33 @@ parseToken AppConfig{..} (Just tkn) time = do
|
|||||||
hasThreeParts :: ByteString -> Either Error ByteString
|
hasThreeParts :: ByteString -> Either Error ByteString
|
||||||
hasThreeParts token = case length $ BS.split (BS.c2w '.') token of
|
hasThreeParts token = case length $ BS.split (BS.c2w '.') token of
|
||||||
3 -> Right token
|
3 -> Right token
|
||||||
n -> Left $ JwtErr $ JwtDecodeError ("Expected 3 parts in JWT; got " <> show n)
|
n -> Left $ JwtErr $ JwtDecodeErr $ UnexpectedParts n
|
||||||
jwtDecodeError :: JWT.JwtError -> JwtError
|
jwtDecodeError :: JWT.JwtError -> JwtError
|
||||||
-- The only errors we can get from JWT.decode function are:
|
-- The only errors we can get from JWT.decode function are:
|
||||||
-- BadAlgorithm
|
-- BadAlgorithm
|
||||||
-- KeyError
|
-- KeyError
|
||||||
-- BadCrypto
|
-- BadCrypto
|
||||||
jwtDecodeError (JWT.KeyError _) = JwtDecodeError "No suitable key or wrong key type"
|
jwtDecodeError (JWT.KeyError m) = JwtDecodeErr $ KeyError m
|
||||||
jwtDecodeError (JWT.BadAlgorithm _) = JwtDecodeError "Wrong or unsupported encoding algorithm"
|
jwtDecodeError (JWT.BadAlgorithm m) = JwtDecodeErr $ BadAlgorithm m
|
||||||
jwtDecodeError JWT.BadCrypto = JwtDecodeError "JWT cryptographic operation failed"
|
jwtDecodeError JWT.BadCrypto = JwtDecodeErr BadCrypto
|
||||||
-- Control never reaches here, the decode function only returns the above three
|
-- Control never reaches here, the decode function only returns the above three
|
||||||
jwtDecodeError _ = JwtDecodeError "JWT couldn't be decoded"
|
jwtDecodeError _ = JwtDecodeErr UnreachableDecodeError
|
||||||
|
|
||||||
verifyClaims :: JWT.JwtContent -> Either JwtError JSON.Value
|
verifyClaims :: JWT.JwtContent -> Either JwtError JSON.Value
|
||||||
verifyClaims (JWT.Jws (_, claims)) = case JSON.decodeStrict claims of
|
verifyClaims (JWT.Jws (_, claims)) = case JSON.decodeStrict claims of
|
||||||
Just jclaims@(JSON.Object mclaims) ->
|
Just jclaims@(JSON.Object mclaims) ->
|
||||||
verifyClaim mclaims "exp" isValidExpClaim "JWT expired" >>
|
verifyClaim mclaims "exp" isValidExpClaim JWTExpired >>
|
||||||
verifyClaim mclaims "nbf" isValidNbfClaim "JWT not yet valid" >>
|
verifyClaim mclaims "nbf" isValidNbfClaim JWTNotYetValid >>
|
||||||
verifyClaim mclaims "iat" isValidIatClaim "JWT issued at future" >>
|
verifyClaim mclaims "iat" isValidIatClaim JWTIssuedAtFuture >>
|
||||||
verifyClaim mclaims "aud" isValidAudClaim "JWT not in audience" >>
|
verifyClaim mclaims "aud" isValidAudClaim JWTNotInAudience >>
|
||||||
return jclaims
|
return jclaims
|
||||||
_ -> Left $ JwtClaimsError "Parsing claims failed"
|
_ -> Left $ JwtClaimsErr ParsingClaimsFailed
|
||||||
-- TODO: We could enable JWE support here (encrypted tokens)
|
-- TODO: We could enable JWE support here (encrypted tokens)
|
||||||
verifyClaims _ = Left $ JwtDecodeError "Unsupported token type"
|
verifyClaims _ = Left $ JwtDecodeErr UnsupportedTokenType
|
||||||
|
|
||||||
verifyClaim mclaims claim func err = do
|
verifyClaim mclaims claim func err = do
|
||||||
isValid <- maybe (Right True) func (KM.lookup claim mclaims)
|
isValid <- maybe (Right True) func (KM.lookup claim mclaims)
|
||||||
unless isValid $ Left $ JwtClaimsError err
|
unless isValid $ Left $ JwtClaimsErr err
|
||||||
|
|
||||||
allowedSkewSeconds = 30 :: Int64
|
allowedSkewSeconds = 30 :: Int64
|
||||||
now = floor . nominalDiffTimeToSeconds $ utcTimeToPOSIXSeconds time
|
now = floor . nominalDiffTimeToSeconds $ utcTimeToPOSIXSeconds time
|
||||||
@@ -104,15 +105,15 @@ parseToken AppConfig{..} (Just tkn) time = do
|
|||||||
|
|
||||||
isValidExpClaim :: JSON.Value -> Either JwtError Bool
|
isValidExpClaim :: JSON.Value -> Either JwtError Bool
|
||||||
isValidExpClaim (JSON.Number secs) = Right $ now <= (sciToInt secs + allowedSkewSeconds)
|
isValidExpClaim (JSON.Number secs) = Right $ now <= (sciToInt secs + allowedSkewSeconds)
|
||||||
isValidExpClaim _ = Left $ JwtClaimsError "The JWT 'exp' claim must be a number"
|
isValidExpClaim _ = Left $ JwtClaimsErr ExpClaimNotNumber
|
||||||
|
|
||||||
isValidNbfClaim :: JSON.Value -> Either JwtError Bool
|
isValidNbfClaim :: JSON.Value -> Either JwtError Bool
|
||||||
isValidNbfClaim (JSON.Number secs) = Right $ now >= (sciToInt secs - allowedSkewSeconds)
|
isValidNbfClaim (JSON.Number secs) = Right $ now >= (sciToInt secs - allowedSkewSeconds)
|
||||||
isValidNbfClaim _ = Left $ JwtClaimsError "The JWT 'nbf' claim must be a number"
|
isValidNbfClaim _ = Left $ JwtClaimsErr NbfClaimNotNumber
|
||||||
|
|
||||||
isValidIatClaim :: JSON.Value -> Either JwtError Bool
|
isValidIatClaim :: JSON.Value -> Either JwtError Bool
|
||||||
isValidIatClaim (JSON.Number secs) = Right $ now >= (sciToInt secs - allowedSkewSeconds)
|
isValidIatClaim (JSON.Number secs) = Right $ now >= (sciToInt secs - allowedSkewSeconds)
|
||||||
isValidIatClaim _ = Left $ JwtClaimsError "The JWT 'iat' claim must be a number"
|
isValidIatClaim _ = Left $ JwtClaimsErr IatClaimNotNumber
|
||||||
|
|
||||||
isValidAudClaim :: JSON.Value -> Either JwtError Bool
|
isValidAudClaim :: JSON.Value -> Either JwtError Bool
|
||||||
isValidAudClaim JSON.Null = Right True -- {"aud": null} is valid for all audiences
|
isValidAudClaim JSON.Null = Right True -- {"aud": null} is valid for all audiences
|
||||||
@@ -120,7 +121,7 @@ parseToken AppConfig{..} (Just tkn) time = do
|
|||||||
isValidAudClaim (JSON.Array arr)
|
isValidAudClaim (JSON.Array arr)
|
||||||
| null arr = Right True -- {"aud": []} is valid for all audiences
|
| null arr = Right True -- {"aud": []} is valid for all audiences
|
||||||
| allStrings arr = Right $ maybe True (\a -> JSON.String a `elem` arr) configJwtAudience
|
| allStrings arr = Right $ maybe True (\a -> JSON.String a `elem` arr) configJwtAudience
|
||||||
isValidAudClaim _ = Left $ JwtClaimsError "The JWT 'aud' claim must be a string or an array of strings"
|
isValidAudClaim _ = Left $ JwtClaimsErr AudClaimNotStringOrArray
|
||||||
|
|
||||||
parseClaims :: Monad m =>
|
parseClaims :: Monad m =>
|
||||||
AppConfig -> JSON.Value -> ExceptT Error m AuthResult
|
AppConfig -> JSON.Value -> ExceptT Error m AuthResult
|
||||||
|
|||||||
+16
-2
@@ -48,7 +48,7 @@ import Data.List.NonEmpty (fromList, toList)
|
|||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Data.Scientific (floatingOrInteger)
|
import Data.Scientific (floatingOrInteger)
|
||||||
import Jose.Jwk (Jwk, JwkSet)
|
import Jose.Jwk (Jwk, JwkSet)
|
||||||
import Network.URI (escapeURIString,
|
import Network.URI (escapeURIString, isURI,
|
||||||
isUnescapedInURIComponent)
|
isUnescapedInURIComponent)
|
||||||
import Numeric (readOct, showOct)
|
import Numeric (readOct, showOct)
|
||||||
import System.Environment (getEnvironment)
|
import System.Environment (getEnvironment)
|
||||||
@@ -281,7 +281,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
|||||||
<*> (fromMaybe "postgresql://" <$> optString "db-uri")
|
<*> (fromMaybe "postgresql://" <$> optString "db-uri")
|
||||||
<*> pure optPath
|
<*> pure optPath
|
||||||
<*> pure Nothing
|
<*> pure Nothing
|
||||||
<*> optString "jwt-aud"
|
<*> optStringOrURI "jwt-aud"
|
||||||
<*> parseRoleClaimKey "jwt-role-claim-key" "role-claim-key"
|
<*> parseRoleClaimKey "jwt-role-claim-key" "role-claim-key"
|
||||||
<*> (fmap encodeUtf8 <$> optString "jwt-secret")
|
<*> (fmap encodeUtf8 <$> optString "jwt-secret")
|
||||||
<*> (fromMaybe False <$> optWithAlias
|
<*> (fromMaybe False <$> optWithAlias
|
||||||
@@ -407,6 +407,20 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
|||||||
optStringEmptyable :: C.Key -> C.Parser C.Config (Maybe Text)
|
optStringEmptyable :: C.Key -> C.Parser C.Config (Maybe Text)
|
||||||
optStringEmptyable k = overrideFromDbOrEnvironment C.optional k coerceText
|
optStringEmptyable k = overrideFromDbOrEnvironment C.optional k coerceText
|
||||||
|
|
||||||
|
optStringOrURI :: C.Key -> C.Parser C.Config (Maybe Text)
|
||||||
|
optStringOrURI k = do
|
||||||
|
stringOrURI <- mfilter (/= "") <$> overrideFromDbOrEnvironment C.optional k coerceText
|
||||||
|
-- If the string contains ':' then it should
|
||||||
|
-- be a valid URI according to RFC 3986
|
||||||
|
case stringOrURI of
|
||||||
|
Just s -> if T.isInfixOf ":" s then validateURI s else return (Just s)
|
||||||
|
Nothing -> return Nothing
|
||||||
|
where
|
||||||
|
validateURI :: Text -> C.Parser C.Config (Maybe Text)
|
||||||
|
validateURI s = if isURI (T.unpack s)
|
||||||
|
then return $ Just s
|
||||||
|
else fail "jwt-aud should be a string or a valid URI"
|
||||||
|
|
||||||
optInt :: (Read i, Integral i) => C.Key -> C.Parser C.Config (Maybe i)
|
optInt :: (Read i, Integral i) => C.Key -> C.Parser C.Config (Maybe i)
|
||||||
optInt k = join <$> overrideFromDbOrEnvironment C.optional k coerceInt
|
optInt k = join <$> overrideFromDbOrEnvironment C.optional k coerceInt
|
||||||
|
|
||||||
|
|||||||
+57
-12
@@ -14,6 +14,8 @@ module PostgREST.Error
|
|||||||
, PgError(..)
|
, PgError(..)
|
||||||
, Error(..)
|
, Error(..)
|
||||||
, JwtError (..)
|
, JwtError (..)
|
||||||
|
, JwtDecodeError(..)
|
||||||
|
, JwtClaimsError(..)
|
||||||
, errorPayload
|
, errorPayload
|
||||||
, status
|
, status
|
||||||
) where
|
) where
|
||||||
@@ -86,7 +88,7 @@ data ApiRequestError
|
|||||||
| QueryParamError QPError
|
| QueryParamError QPError
|
||||||
| RelatedOrderNotToOne Text Text
|
| RelatedOrderNotToOne Text Text
|
||||||
| UnacceptableFilter Text
|
| UnacceptableFilter Text
|
||||||
| UnacceptableSchema [Text]
|
| UnacceptableSchema Text [Text]
|
||||||
| UnsupportedMethod ByteString
|
| UnsupportedMethod ByteString
|
||||||
| GucHeadersError
|
| GucHeadersError
|
||||||
| GucStatusError
|
| GucStatusError
|
||||||
@@ -194,7 +196,7 @@ instance ErrorBody ApiRequestError where
|
|||||||
message (InvalidBody errorMessage) = T.decodeUtf8 errorMessage
|
message (InvalidBody errorMessage) = T.decodeUtf8 errorMessage
|
||||||
message (InvalidRange _) = "Requested range not satisfiable"
|
message (InvalidRange _) = "Requested range not satisfiable"
|
||||||
message InvalidFilters = "Filters must include all and only primary key columns with 'eq' operators"
|
message InvalidFilters = "Filters must include all and only primary key columns with 'eq' operators"
|
||||||
message (UnacceptableSchema schemas) = "The schema must be one of the following: " <> T.intercalate ", " schemas
|
message (UnacceptableSchema sch _) = "Invalid schema: " <> sch
|
||||||
message (MediaTypeError cts) = "None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)
|
message (MediaTypeError cts) = "None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)
|
||||||
message (NotEmbedded resource) = "'" <> resource <> "' is not an embedded resource in this request"
|
message (NotEmbedded resource) = "'" <> resource <> "' is not an embedded resource in this request"
|
||||||
message GucHeadersError = "response.headers guc must be a JSON array composed of objects with a single key and a string value"
|
message GucHeadersError = "response.headers guc must be a JSON array composed of objects with a single key and a string value"
|
||||||
@@ -234,6 +236,7 @@ instance ErrorBody ApiRequestError where
|
|||||||
-- HINT: Maybe JSON.Value
|
-- HINT: Maybe JSON.Value
|
||||||
hint (NotEmbedded resource) = Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter."
|
hint (NotEmbedded resource) = Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter."
|
||||||
hint (PGRSTParseError raiseErr) = Just $ JSON.String $ pgrstParseErrorHint raiseErr
|
hint (PGRSTParseError raiseErr) = Just $ JSON.String $ pgrstParseErrorHint raiseErr
|
||||||
|
hint (UnacceptableSchema _ schemas) = Just $ JSON.String $ "Only the following schemas are exposed: " <> T.intercalate ", " schemas
|
||||||
|
|
||||||
hint _ = Nothing
|
hint _ = Nothing
|
||||||
|
|
||||||
@@ -647,10 +650,32 @@ data Error
|
|||||||
deriving Show
|
deriving Show
|
||||||
|
|
||||||
data JwtError
|
data JwtError
|
||||||
= JwtDecodeError Text
|
= JwtDecodeErr JwtDecodeError
|
||||||
| JwtSecretMissing
|
| JwtSecretMissing
|
||||||
| JwtTokenRequired
|
| JwtTokenRequired
|
||||||
| JwtClaimsError Text
|
| JwtClaimsErr JwtClaimsError
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
data JwtDecodeError
|
||||||
|
= EmptyAuthHeader
|
||||||
|
| UnexpectedParts Int
|
||||||
|
| KeyError Text
|
||||||
|
| BadAlgorithm Text
|
||||||
|
| BadCrypto
|
||||||
|
| UnsupportedTokenType
|
||||||
|
| UnreachableDecodeError
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
data JwtClaimsError
|
||||||
|
= JWTExpired
|
||||||
|
| JWTNotYetValid
|
||||||
|
| JWTIssuedAtFuture
|
||||||
|
| JWTNotInAudience
|
||||||
|
| ParsingClaimsFailed
|
||||||
|
| ExpClaimNotNumber
|
||||||
|
| NbfClaimNotNumber
|
||||||
|
| IatClaimNotNumber
|
||||||
|
| AudClaimNotStringOrArray
|
||||||
deriving Show
|
deriving Show
|
||||||
|
|
||||||
instance PgrstError Error where
|
instance PgrstError Error where
|
||||||
@@ -696,14 +721,14 @@ instance ErrorBody Error where
|
|||||||
hint (PgErr err) = hint err
|
hint (PgErr err) = hint err
|
||||||
|
|
||||||
instance PgrstError JwtError where
|
instance PgrstError JwtError where
|
||||||
status JwtDecodeError{} = HTTP.unauthorized401
|
status JwtDecodeErr{} = HTTP.unauthorized401
|
||||||
status JwtSecretMissing = HTTP.status500
|
status JwtSecretMissing = HTTP.status500
|
||||||
status JwtTokenRequired = HTTP.unauthorized401
|
status JwtTokenRequired = HTTP.unauthorized401
|
||||||
status JwtClaimsError{} = HTTP.unauthorized401
|
status JwtClaimsErr{} = HTTP.unauthorized401
|
||||||
|
|
||||||
headers (JwtDecodeError m) = [invalidTokenHeader m]
|
headers e@(JwtDecodeErr _) = [invalidTokenHeader $ message e]
|
||||||
headers JwtTokenRequired = [requiredTokenHeader]
|
headers JwtTokenRequired = [requiredTokenHeader]
|
||||||
headers (JwtClaimsError m) = [invalidTokenHeader m]
|
headers e@(JwtClaimsErr _) = [invalidTokenHeader $ message e]
|
||||||
headers _ = mempty
|
headers _ = mempty
|
||||||
|
|
||||||
instance JSON.ToJSON JwtError where
|
instance JSON.ToJSON JwtError where
|
||||||
@@ -712,15 +737,35 @@ instance JSON.ToJSON JwtError where
|
|||||||
|
|
||||||
instance ErrorBody JwtError where
|
instance ErrorBody JwtError where
|
||||||
code JwtSecretMissing = "PGRST300"
|
code JwtSecretMissing = "PGRST300"
|
||||||
code (JwtDecodeError _) = "PGRST301"
|
code (JwtDecodeErr _) = "PGRST301"
|
||||||
code JwtTokenRequired = "PGRST302"
|
code JwtTokenRequired = "PGRST302"
|
||||||
code (JwtClaimsError _) = "PGRST303"
|
code (JwtClaimsErr _) = "PGRST303"
|
||||||
|
|
||||||
message JwtSecretMissing = "Server lacks JWT secret"
|
message JwtSecretMissing = "Server lacks JWT secret"
|
||||||
message (JwtDecodeError msg) = msg
|
message (JwtDecodeErr e) = case e of
|
||||||
|
EmptyAuthHeader -> "Empty JWT is sent in Authorization header"
|
||||||
|
UnexpectedParts n -> "Expected 3 parts in JWT; got " <> show n
|
||||||
|
KeyError _ -> "No suitable key or wrong key type"
|
||||||
|
BadAlgorithm _ -> "Wrong or unsupported encoding algorithm"
|
||||||
|
BadCrypto -> "JWT cryptographic operation failed"
|
||||||
|
UnsupportedTokenType -> "Unsupported token type"
|
||||||
|
UnreachableDecodeError -> "JWT couldn't be decoded"
|
||||||
message JwtTokenRequired = "Anonymous access is disabled"
|
message JwtTokenRequired = "Anonymous access is disabled"
|
||||||
message (JwtClaimsError msg) = msg
|
message (JwtClaimsErr e) = case e of
|
||||||
|
JWTExpired -> "JWT expired"
|
||||||
|
JWTNotYetValid -> "JWT not yet valid"
|
||||||
|
JWTIssuedAtFuture -> "JWT issued at future"
|
||||||
|
JWTNotInAudience -> "JWT not in audience"
|
||||||
|
ParsingClaimsFailed -> "Parsing claims failed"
|
||||||
|
ExpClaimNotNumber -> "The JWT 'exp' claim must be a number"
|
||||||
|
NbfClaimNotNumber -> "The JWT 'nbf' claim must be a number"
|
||||||
|
IatClaimNotNumber -> "The JWT 'iat' claim must be a number"
|
||||||
|
AudClaimNotStringOrArray -> "The JWT 'aud' claim must be a string or an array of strings"
|
||||||
|
|
||||||
|
details (JwtDecodeErr jde) = case jde of
|
||||||
|
KeyError dets -> Just $ JSON.String dets
|
||||||
|
BadAlgorithm dets -> Just $ JSON.String dets
|
||||||
|
_ -> Nothing
|
||||||
details _ = Nothing
|
details _ = Nothing
|
||||||
|
|
||||||
hint _ = Nothing
|
hint _ = Nothing
|
||||||
|
|||||||
@@ -13,5 +13,9 @@ resolveHost sock = do
|
|||||||
sn <- NS.getSocketName sock
|
sn <- NS.getSocketName sock
|
||||||
case sn of
|
case sn of
|
||||||
NS.SockAddrInet _ hostAddr -> pure $ Just $ fromString $ show $ fromHostAddress hostAddr
|
NS.SockAddrInet _ hostAddr -> pure $ Just $ fromString $ show $ fromHostAddress hostAddr
|
||||||
NS.SockAddrInet6 _ _ hostAddr6 _ -> pure $ Just $ fromString $ show $ fromHostAddress6 hostAddr6
|
-- The IPv6 addresses are wrapped in [] brackets. This is done in accordance
|
||||||
|
-- to RFC 3986 (https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2).
|
||||||
|
-- In short, we did this to have a clear separation between the port and host
|
||||||
|
-- because the components of an IPv6 are separated with the ':' character.
|
||||||
|
NS.SockAddrInet6 _ _ hostAddr6 _ -> pure $ Just $ fromString $ "[" ++ show (fromHostAddress6 hostAddr6) ++ "]"
|
||||||
_ -> pure Nothing
|
_ -> pure Nothing
|
||||||
|
|||||||
@@ -108,11 +108,8 @@ observationMessage = \case
|
|||||||
DBListenStart channel -> do
|
DBListenStart channel -> do
|
||||||
"Listening for database notifications on the " <> show channel <> " channel"
|
"Listening for database notifications on the " <> show channel <> " channel"
|
||||||
DBListenFail channel listenErr ->
|
DBListenFail channel listenErr ->
|
||||||
"Failed listening for database notifications on the " <> show channel <> " channel. " <> (
|
"Failed listening for database notifications on the " <> show channel <> " channel. " <>
|
||||||
case listenErr of
|
either showListenerConnError showListenerException listenErr
|
||||||
Left err -> show err
|
|
||||||
Right err -> showListenerError err
|
|
||||||
)
|
|
||||||
DBListenRetry delay ->
|
DBListenRetry delay ->
|
||||||
"Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..."
|
"Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..."
|
||||||
DBListenerGotSCacheMsg channel ->
|
DBListenerGotSCacheMsg channel ->
|
||||||
@@ -157,8 +154,11 @@ observationMessage = \case
|
|||||||
|
|
||||||
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err
|
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err
|
||||||
|
|
||||||
showListenerError :: Either SomeException () -> Text
|
showOnSingleLine txt = T.intercalate " " $ T.filter (/= '\t') <$> T.lines txt -- the errors from hasql-notifications come intercalated with "\t\n"
|
||||||
showListenerError (Right _) = "Failed getting notifications" -- should not happen as the listener will never finish (hasql-notifications uses `forever` internally) with a Right result
|
|
||||||
showListenerError (Left e) =
|
showListenerConnError :: SQL.ConnectionError -> Text
|
||||||
let showOnSingleLine txt = T.intercalate " " $ T.filter (/= '\t') <$> T.lines txt in -- the errors from hasql-notifications come intercalated with "\t\n"
|
showListenerConnError = maybe "Connection error" (showOnSingleLine . T.decodeUtf8)
|
||||||
showOnSingleLine $ show e
|
|
||||||
|
showListenerException :: Either SomeException () -> Text
|
||||||
|
showListenerException (Right _) = "Failed getting notifications" -- should not happen as the listener will never finish (hasql-notifications uses `forever` internally) with a Right result
|
||||||
|
showListenerException (Left e) = showOnSingleLine $ show e
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ data ResolverContext = ResolverContext
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolveColumnField :: Column -> Maybe ToTsVector -> CoercibleField
|
resolveColumnField :: Column -> Maybe ToTsVector -> CoercibleField
|
||||||
resolveColumnField col toTsV = CoercibleField (colName col) mempty False toTsV (colNominalType col) Nothing (colDefault col) False
|
resolveColumnField col toTsV = CoercibleField (colName col) mempty False toTsV (colNominalType col) (colType col) Nothing (colDefault col) False
|
||||||
|
|
||||||
resolveTableFieldName :: Table -> FieldName -> Maybe ToTsVector -> CoercibleField
|
resolveTableFieldName :: Table -> FieldName -> Maybe ToTsVector -> CoercibleField
|
||||||
resolveTableFieldName table fieldName toTsV=
|
resolveTableFieldName table fieldName toTsV=
|
||||||
@@ -294,7 +294,7 @@ resolveTypeOrUnknown ResolverContext{..} (fn, jp) toTsV =
|
|||||||
cf@CoercibleField{cfIRType="json"} -> cf{cfJsonPath=jp, cfToJson=False}
|
cf@CoercibleField{cfIRType="json"} -> cf{cfJsonPath=jp, cfToJson=False}
|
||||||
cf@CoercibleField{cfIRType="jsonb"} -> cf{cfJsonPath=jp, cfToJson=False}
|
cf@CoercibleField{cfIRType="jsonb"} -> cf{cfJsonPath=jp, cfToJson=False}
|
||||||
-- Do not apply to_tsvector to tsvector types
|
-- Do not apply to_tsvector to tsvector types
|
||||||
cf@CoercibleField{cfIRType="tsvector"} -> cf{cfJsonPath=jp, cfToJson=True, cfToTsVector=Nothing}
|
cf@CoercibleField{cfBaseType="tsvector"} -> cf{cfJsonPath=jp, cfToJson=True, cfToTsVector=Nothing}
|
||||||
-- other types will get converted `to_jsonb(col)->attr`, even unknown types
|
-- other types will get converted `to_jsonb(col)->attr`, even unknown types
|
||||||
cf -> cf{cfJsonPath=jp, cfToJson=True}
|
cf -> cf{cfJsonPath=jp, cfToJson=True}
|
||||||
where
|
where
|
||||||
@@ -891,7 +891,7 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
|||||||
-- where_ = [
|
-- where_ = [
|
||||||
-- CoercibleStmnt (
|
-- CoercibleStmnt (
|
||||||
-- CoercibleFilter {
|
-- CoercibleFilter {
|
||||||
-- field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson=False, cfToTsVector = Nothing, cfIRType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False},
|
-- field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson=False, cfToTsVector = Nothing, cfIRType = "", cfBaseType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False},
|
||||||
-- opExpr = op
|
-- opExpr = op
|
||||||
-- }
|
-- }
|
||||||
-- )
|
-- )
|
||||||
@@ -907,7 +907,7 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
|||||||
-- Don't do anything to the filter if there's no embedding (a subtree) on projects. Assume it's a normal filter.
|
-- Don't do anything to the filter if there's no embedding (a subtree) on projects. Assume it's a normal filter.
|
||||||
--
|
--
|
||||||
-- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nullOp [])
|
-- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nullOp [])
|
||||||
-- Right [CoercibleStmnt (CoercibleFilter {field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson = False, cfToTsVector = Nothing, cfIRType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False}, opExpr = OpExpr True (Is IsNull)})]
|
-- Right [CoercibleStmnt (CoercibleFilter {field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson = False, cfToTsVector = Nothing, cfIRType = "", cfBaseType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False}, opExpr = OpExpr True (Is IsNull)})]
|
||||||
--
|
--
|
||||||
-- If there's an embedding on projects, then change the filter to use the internal aggregate name (`clients_projects_1`) so the filter can succeed later.
|
-- If there's an embedding on projects, then change the filter to use the internal aggregate name (`clients_projects_1`) so the filter can succeed later.
|
||||||
--
|
--
|
||||||
@@ -926,7 +926,7 @@ addNullEmbedFilters (Node rp@ReadPlan{where_=curLogic} forest) = do
|
|||||||
newNullFilters rPlans = \case
|
newNullFilters rPlans = \case
|
||||||
(CoercibleExpr b lOp trees) ->
|
(CoercibleExpr b lOp trees) ->
|
||||||
CoercibleExpr b lOp <$> (newNullFilters rPlans `traverse` trees)
|
CoercibleExpr b lOp <$> (newNullFilters rPlans `traverse` trees)
|
||||||
flt@(CoercibleStmnt (CoercibleFilter (CoercibleField fld [] _ _ _ _ _ _) opExpr)) ->
|
flt@(CoercibleStmnt (CoercibleFilter CoercibleField{cfName=fld, cfJsonPath=[]} opExpr)) ->
|
||||||
let foundRP = find (\ReadPlan{relName, relAlias} -> fld == fromMaybe relName relAlias) rPlans in
|
let foundRP = find (\ReadPlan{relName, relAlias} -> fld == fromMaybe relName relAlias) rPlans in
|
||||||
case (foundRP, opExpr) of
|
case (foundRP, opExpr) of
|
||||||
(Just ReadPlan{relAggAlias}, OpExpr b (Is IsNull)) -> Right $ CoercibleStmnt $ CoercibleFilterNullEmbed b relAggAlias
|
(Just ReadPlan{relAggAlias}, OpExpr b (Is IsNull)) -> Right $ CoercibleStmnt $ CoercibleFilterNullEmbed b relAggAlias
|
||||||
|
|||||||
@@ -44,13 +44,14 @@ data CoercibleField = CoercibleField
|
|||||||
, cfToJson :: Bool
|
, cfToJson :: Bool
|
||||||
, cfToTsVector :: Maybe ToTsVector -- ^ If the field should be converted using to_tsvector(<language>, <field>)
|
, cfToTsVector :: Maybe ToTsVector -- ^ If the field should be converted using to_tsvector(<language>, <field>)
|
||||||
, cfIRType :: Text -- ^ The native Postgres type of the field, the intermediate (IR) type before mapping.
|
, cfIRType :: Text -- ^ The native Postgres type of the field, the intermediate (IR) type before mapping.
|
||||||
|
, cfBaseType :: Text -- ^ The base type of the field in case of domains, or just the type otherwise (without modifiers in case of pg_catalog types)
|
||||||
, cfTransform :: Maybe TransformerProc -- ^ The optional mapping from irType -> targetType.
|
, cfTransform :: Maybe TransformerProc -- ^ The optional mapping from irType -> targetType.
|
||||||
, cfDefault :: Maybe Text
|
, cfDefault :: Maybe Text
|
||||||
, cfFullRow :: Bool -- ^ True if the field represents the whole selected row. Used in spread rels: instead of COUNT(*), it does a COUNT(<row>) in order to not mix with other spreaded resources.
|
, cfFullRow :: Bool -- ^ True if the field represents the whole selected row. Used in spread rels: instead of COUNT(*), it does a COUNT(<row>) in order to not mix with other spreaded resources.
|
||||||
} deriving (Eq, Show)
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
unknownField :: FieldName -> JsonPath -> CoercibleField
|
unknownField :: FieldName -> JsonPath -> CoercibleField
|
||||||
unknownField name path = CoercibleField name path False Nothing "" Nothing Nothing False
|
unknownField name path = CoercibleField name path False Nothing "" "" Nothing Nothing False
|
||||||
|
|
||||||
-- | Like an API request LogicTree, but with coercible field information.
|
-- | Like an API request LogicTree, but with coercible field information.
|
||||||
data CoercibleLogicTree
|
data CoercibleLogicTree
|
||||||
|
|||||||
@@ -70,21 +70,21 @@ readPlanToQuery node@(Node ReadPlan{select,from=mainQi,fromAlias,where_=logicFor
|
|||||||
|
|
||||||
getJoinSelects :: ReadPlanTree -> [SQL.Snippet]
|
getJoinSelects :: ReadPlanTree -> [SQL.Snippet]
|
||||||
getJoinSelects (Node ReadPlan{relSelect} _) =
|
getJoinSelects (Node ReadPlan{relSelect} _) =
|
||||||
mapMaybe relSelectToSnippet relSelect
|
join $ map relSelectToSnippet relSelect
|
||||||
where
|
where
|
||||||
relSelectToSnippet :: RelSelectField -> Maybe SQL.Snippet
|
relSelectToSnippet :: RelSelectField -> [SQL.Snippet]
|
||||||
relSelectToSnippet fld =
|
relSelectToSnippet fld =
|
||||||
let aggAlias = pgFmtIdent $ rsAggAlias fld
|
let aggAlias = pgFmtIdent $ rsAggAlias fld
|
||||||
in
|
in
|
||||||
case fld of
|
case fld of
|
||||||
JsonEmbed{rsEmptyEmbed = True} ->
|
JsonEmbed{rsEmptyEmbed = True} ->
|
||||||
Nothing
|
[]
|
||||||
JsonEmbed{rsSelName, rsEmbedMode = JsonObject} ->
|
JsonEmbed{rsSelName, rsEmbedMode = JsonObject} ->
|
||||||
Just $ "row_to_json(" <> aggAlias <> ".*)::jsonb AS " <> pgFmtIdent rsSelName
|
["row_to_json(" <> aggAlias <> ".*)::jsonb AS " <> pgFmtIdent rsSelName]
|
||||||
JsonEmbed{rsSelName, rsEmbedMode = JsonArray} ->
|
JsonEmbed{rsSelName, rsEmbedMode = JsonArray} ->
|
||||||
Just $ "COALESCE( " <> aggAlias <> "." <> aggAlias <> ", '[]') AS " <> pgFmtIdent rsSelName
|
["COALESCE( " <> aggAlias <> "." <> aggAlias <> ", '[]') AS " <> pgFmtIdent rsSelName]
|
||||||
Spread{rsSpreadSel, rsAggAlias} ->
|
Spread{rsSpreadSel, rsAggAlias} ->
|
||||||
Just $ intercalateSnippet ", " (pgFmtSpreadSelectItem rsAggAlias <$> rsSpreadSel)
|
pgFmtSpreadSelectItem rsAggAlias <$> rsSpreadSel
|
||||||
|
|
||||||
getJoins :: ReadPlanTree -> [SQL.Snippet]
|
getJoins :: ReadPlanTree -> [SQL.Snippet]
|
||||||
getJoins (Node _ []) = []
|
getJoins (Node _ []) = []
|
||||||
@@ -182,7 +182,7 @@ callPlanToQuery (FunctionCall qi params arguments returnsScalar returnsSetOfScal
|
|||||||
KeyParams [] -> "FROM " <> callIt mempty
|
KeyParams [] -> "FROM " <> callIt mempty
|
||||||
KeyParams prms -> case arguments of
|
KeyParams prms -> case arguments of
|
||||||
DirectArgs args -> "FROM " <> callIt (fmtArgs prms args)
|
DirectArgs args -> "FROM " <> callIt (fmtArgs prms args)
|
||||||
JsonArgs json -> fromJsonBodyF json ((\p -> CoercibleField (ppName p) mempty False Nothing (ppTypeMaxLength p) Nothing Nothing False) <$> prms) False True False <> ", " <>
|
JsonArgs json -> fromJsonBodyF json ((\p -> CoercibleField (ppName p) mempty False Nothing (ppTypeMaxLength p) mempty Nothing Nothing False) <$> prms) False True False <> ", " <>
|
||||||
"LATERAL " <> callIt (fmtParams prms)
|
"LATERAL " <> callIt (fmtParams prms)
|
||||||
|
|
||||||
callIt :: SQL.Snippet -> SQL.Snippet
|
callIt :: SQL.Snippet -> SQL.Snippet
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
|
|||||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||||
Relationship (..),
|
Relationship (..),
|
||||||
RelationshipsMap)
|
RelationshipsMap)
|
||||||
import PostgREST.SchemaCache.Routine (Routine (..),
|
import PostgREST.SchemaCache.Routine (FuncVolatility (..),
|
||||||
|
Routine (..),
|
||||||
RoutineParam (..))
|
RoutineParam (..))
|
||||||
import PostgREST.SchemaCache.Table (Column (..), Table (..),
|
import PostgREST.SchemaCache.Table (Column (..), Table (..),
|
||||||
TablesMap,
|
TablesMap,
|
||||||
@@ -170,8 +171,9 @@ makePreferParam ts =
|
|||||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||||
& in_ .~ ParamHeader
|
& in_ .~ ParamHeader
|
||||||
& type_ ?~ SwaggerString
|
& type_ ?~ SwaggerString
|
||||||
& enum_ .~ JSON.decode (JSON.encode $ foldl (<>) [] (val <$> ts)))
|
& enum_ .~ if null enu then Nothing else JSON.decode (JSON.encode enu))
|
||||||
where
|
where
|
||||||
|
enu = foldl (<>) [] (val <$> ts)
|
||||||
val :: Text -> [Text]
|
val :: Text -> [Text]
|
||||||
val = \case
|
val = \case
|
||||||
"count" -> ["count=none"]
|
"count" -> ["count=none"]
|
||||||
@@ -355,9 +357,9 @@ makeProcPathItem pd = ("/rpc/" ++ toS (pdName pd), pe)
|
|||||||
& parameters .~ makeProcGetParams (pdParams pd)
|
& parameters .~ makeProcGetParams (pdParams pd)
|
||||||
postOp = procOp
|
postOp = procOp
|
||||||
& parameters .~ makeProcPostParams pd
|
& parameters .~ makeProcPostParams pd
|
||||||
pe = (mempty :: PathItem)
|
pe = case pdVolatility pd of
|
||||||
& get ?~ getOp
|
Volatile -> (mempty :: PathItem) & post ?~ postOp
|
||||||
& post ?~ postOp
|
_ -> (mempty :: PathItem) & get ?~ getOp & post ?~ postOp
|
||||||
|
|
||||||
makeRootPathItem :: (FilePath, PathItem)
|
makeRootPathItem :: (FilePath, PathItem)
|
||||||
makeRootPathItem = ("/", p)
|
makeRootPathItem = ("/", p)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ prettyVersion =
|
|||||||
docsVersion :: Text
|
docsVersion :: Text
|
||||||
docsVersion
|
docsVersion
|
||||||
| isPreRelease = "latest"
|
| isPreRelease = "latest"
|
||||||
| otherwise = "v" <> (T.intercalate "." . map show . take 1 $ version)
|
| otherwise = "v" <> T.intercalate "." (take 1 version)
|
||||||
|
|
||||||
|
|
||||||
-- | Versions with two components (e.g., '1.1') are treated as pre-releases.
|
-- | Versions with two components (e.g., '1.1') are treated as pre-releases.
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
# We're keeping this at 21.7 for freebsd right now, because:
|
|
||||||
# Error: [S-9443]
|
|
||||||
# No setup information found for ghc-9.6.4 on your platform. This probably means a GHC binary
|
|
||||||
# distribution has not yet been added for OS key freebsd64-ino64. Supported versions:
|
|
||||||
# ghc-8.4.3, ghc-8.4.4, ghc-8.6.2, ghc-8.6.3, ghc-8.6.4, ghc-8.6.5, ghc-8.8.1, ghc-8.8.2,
|
|
||||||
# ghc-8.8.3, ghc-8.8.4, ghc-8.10.3, ghc-8.10.4, ghc-8.10.6, ghc-8.10.7, ghc-9.0.2, ghc-9.2.5,
|
|
||||||
# ghc-9.2.6, ghc-9.2.7, ghc-9.2.8 and ghc-9.4.5.
|
|
||||||
resolver: lts-21.7 # 2023-08-14, GHC 9.4.5
|
|
||||||
|
|
||||||
nix:
|
|
||||||
packages:
|
|
||||||
- pcre
|
|
||||||
- pkgconfig
|
|
||||||
- postgresql
|
|
||||||
- zlib
|
|
||||||
# disable pure by default so that the test environment can be passed
|
|
||||||
pure: false
|
|
||||||
|
|
||||||
extra-deps:
|
|
||||||
- configurator-pg-0.2.10
|
|
||||||
- fuzzyset-0.2.4
|
|
||||||
- hasql-notifications-0.2.2.2
|
|
||||||
- hasql-pool-1.0.1
|
|
||||||
- postgresql-libpq-0.10.1.0
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
# This file was autogenerated by Stack.
|
|
||||||
# You should not edit this file by hand.
|
|
||||||
# For more information, please see the documentation at:
|
|
||||||
# https://docs.haskellstack.org/en/stable/lock_files
|
|
||||||
|
|
||||||
packages:
|
|
||||||
- completed:
|
|
||||||
hackage: configurator-pg-0.2.10@sha256:dbb9381c4f491b838214289c59d465eff99f6b569334a98ccf836548970e0f49,2786
|
|
||||||
pantry-tree:
|
|
||||||
sha256: a6dc0197c8c8515877ff579c088511edcf2d1090d9a73ac618d884101d6cbed8
|
|
||||||
size: 2463
|
|
||||||
original:
|
|
||||||
hackage: configurator-pg-0.2.10
|
|
||||||
- completed:
|
|
||||||
hackage: fuzzyset-0.2.4@sha256:f1b6de8bf33277bf6255207541d65028f1f1ea93af5541b654c86b5674995485,1618
|
|
||||||
pantry-tree:
|
|
||||||
sha256: cee68e8d88f530e9e0588b81b260236936fe3318ef9a66e9f43f680b4cd5f76e
|
|
||||||
size: 574
|
|
||||||
original:
|
|
||||||
hackage: fuzzyset-0.2.4
|
|
||||||
- completed:
|
|
||||||
hackage: hasql-notifications-0.2.2.2@sha256:d1d6bc0d3ee5e418fc12ea023b78739e0decba6c34e2b43bec55b89e18bd4412,2025
|
|
||||||
pantry-tree:
|
|
||||||
sha256: 83a9cbb179b1efd0b2acd6509583c7afcdbe63469ab033d8581d48d675a80b44
|
|
||||||
size: 452
|
|
||||||
original:
|
|
||||||
hackage: hasql-notifications-0.2.2.2
|
|
||||||
- completed:
|
|
||||||
hackage: hasql-pool-1.0.1@sha256:3cfb4c7153a6c536ac7e126c17723e6d26ee03794954deed2d72bcc826d05a40,2302
|
|
||||||
pantry-tree:
|
|
||||||
sha256: d98e1269bdd60989b0eb0b84e1d5357eaa9f92821439d9f206663b7251ee95b2
|
|
||||||
size: 799
|
|
||||||
original:
|
|
||||||
hackage: hasql-pool-1.0.1
|
|
||||||
- completed:
|
|
||||||
hackage: postgresql-libpq-0.10.1.0@sha256:6b580c9d5068e78eecc13e655b2885c8e79cdacfca513c5d1e5a6b9dc61d9758,3166
|
|
||||||
pantry-tree:
|
|
||||||
sha256: ae81e7628a8f3d1ef33ace71fa0845c073c003ca7f1150cc9d9ba1e55fc84236
|
|
||||||
size: 1096
|
|
||||||
original:
|
|
||||||
hackage: postgresql-libpq-0.10.1.0
|
|
||||||
snapshots:
|
|
||||||
- completed:
|
|
||||||
sha256: 23bb9bb355bfdb1635252e120a29b712f0d5e8a6c6a65c5ab5bd6692f46c438e
|
|
||||||
size: 640457
|
|
||||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/21/7.yaml
|
|
||||||
original: lts-21.7
|
|
||||||
+3
-5
@@ -1,10 +1,9 @@
|
|||||||
resolver: lts-22.41 # 2024-11-10, GHC 9.6.6
|
resolver: lts-22.44 # 2025-05-02, GHC 9.6.7
|
||||||
|
|
||||||
nix:
|
nix:
|
||||||
packages:
|
packages:
|
||||||
- pcre
|
- libpq
|
||||||
- pkgconfig
|
- pkg-config
|
||||||
- postgresql
|
|
||||||
- zlib
|
- zlib
|
||||||
# disable pure by default so that the test environment can be passed
|
# disable pure by default so that the test environment can be passed
|
||||||
pure: false
|
pure: false
|
||||||
@@ -14,4 +13,3 @@ extra-deps:
|
|||||||
- hasql-pool-1.0.1
|
- hasql-pool-1.0.1
|
||||||
- jose-jwt-0.10.0
|
- jose-jwt-0.10.0
|
||||||
- postgresql-libpq-0.10.1.0
|
- postgresql-libpq-0.10.1.0
|
||||||
- hasql-notifications-0.2.2.2
|
|
||||||
|
|||||||
+5
-12
@@ -1,7 +1,7 @@
|
|||||||
# This file was autogenerated by Stack.
|
# This file was autogenerated by Stack.
|
||||||
# You should not edit this file by hand.
|
# You should not edit this file by hand.
|
||||||
# For more information, please see the documentation at:
|
# For more information, please see the documentation at:
|
||||||
# https://docs.haskellstack.org/en/stable/lock_files
|
# https://docs.haskellstack.org/en/stable/topics/lock_files
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
- completed:
|
- completed:
|
||||||
@@ -32,16 +32,9 @@ packages:
|
|||||||
size: 1096
|
size: 1096
|
||||||
original:
|
original:
|
||||||
hackage: postgresql-libpq-0.10.1.0
|
hackage: postgresql-libpq-0.10.1.0
|
||||||
- completed:
|
|
||||||
hackage: hasql-notifications-0.2.2.2@sha256:d1d6bc0d3ee5e418fc12ea023b78739e0decba6c34e2b43bec55b89e18bd4412,2025
|
|
||||||
pantry-tree:
|
|
||||||
sha256: 83a9cbb179b1efd0b2acd6509583c7afcdbe63469ab033d8581d48d675a80b44
|
|
||||||
size: 452
|
|
||||||
original:
|
|
||||||
hackage: hasql-notifications-0.2.2.2
|
|
||||||
snapshots:
|
snapshots:
|
||||||
- completed:
|
- completed:
|
||||||
sha256: 1e32b51d9082fdf6f3bd92accc9dfffd4ddaf406404427fb10bf76d2bc03cbbb
|
sha256: 238fa745b64f91184f9aa518fe04bdde6552533d169b0da5256670df83a0f1a9
|
||||||
size: 720263
|
size: 721141
|
||||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/41.yaml
|
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/44.yaml
|
||||||
original: lts-22.41
|
original: lts-22.44
|
||||||
|
|||||||
@@ -41,12 +41,11 @@ cli:
|
|||||||
use_defaultenv: true
|
use_defaultenv: true
|
||||||
env:
|
env:
|
||||||
PGRST_SERVER_UNIX_SOCKET_MODE: '778'
|
PGRST_SERVER_UNIX_SOCKET_MODE: '778'
|
||||||
# TODO: Bug needs to be fixed
|
- name: invalid jwt-aud
|
||||||
# - name: invalid jwt-aud
|
expect: error
|
||||||
# expect: error
|
use_defaultenv: true
|
||||||
# use_defaultenv: true
|
env:
|
||||||
# env:
|
PGRST_JWT_AUD: 'http://%%localhorst.invalid'
|
||||||
# PGRST_JWT_AUD: 'htp:/@@localhorst.invalid'
|
|
||||||
- name: invalid log-level
|
- name: invalid log-level
|
||||||
expect: error
|
expect: error
|
||||||
use_defaultenv: true
|
use_defaultenv: true
|
||||||
|
|||||||
+13
-2
@@ -96,7 +96,9 @@ def run(
|
|||||||
if port:
|
if port:
|
||||||
env["PGRST_SERVER_PORT"] = str(port)
|
env["PGRST_SERVER_PORT"] = str(port)
|
||||||
env["PGRST_SERVER_HOST"] = host or "localhost"
|
env["PGRST_SERVER_HOST"] = host or "localhost"
|
||||||
baseurl = f"http://localhost:{port}"
|
# When constructing IPv6 address, host address should be bracketed like [host]
|
||||||
|
apihost = f"[{host}]" if host and is_ipv6(host) else "localhost"
|
||||||
|
baseurl = f"http://{apihost}:{port}"
|
||||||
else:
|
else:
|
||||||
socketfile = pathlib.Path(tmpdir) / "postgrest.sock"
|
socketfile = pathlib.Path(tmpdir) / "postgrest.sock"
|
||||||
env["PGRST_SERVER_UNIX_SOCKET"] = str(socketfile)
|
env["PGRST_SERVER_UNIX_SOCKET"] = str(socketfile)
|
||||||
@@ -104,7 +106,8 @@ def run(
|
|||||||
|
|
||||||
adminport = freeport(port)
|
adminport = freeport(port)
|
||||||
env["PGRST_ADMIN_SERVER_PORT"] = str(adminport)
|
env["PGRST_ADMIN_SERVER_PORT"] = str(adminport)
|
||||||
adminurl = f"http://localhost:{adminport}"
|
adminhost = f"[{host}]" if host and is_ipv6(host) else "localhost"
|
||||||
|
adminurl = f"http://{adminhost}:{adminport}"
|
||||||
|
|
||||||
command = [POSTGREST_BIN]
|
command = [POSTGREST_BIN]
|
||||||
env["HPCTIXFILE"] = hpctixfile()
|
env["HPCTIXFILE"] = hpctixfile()
|
||||||
@@ -218,3 +221,11 @@ def sleep_pool_connection(url, seconds):
|
|||||||
session.get(url + f"/rpc/sleep?seconds={seconds}", timeout=0.1)
|
session.get(url + f"/rpc/sleep?seconds={seconds}", timeout=0.1)
|
||||||
except requests.exceptions.ReadTimeout:
|
except requests.exceptions.ReadTimeout:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def is_ipv6(addr):
|
||||||
|
try:
|
||||||
|
socket.inet_pton(socket.AF_INET6, addr)
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|||||||
@@ -278,3 +278,15 @@ def test_schema_cache_snapshot(baseenv, key, snapshot_yaml):
|
|||||||
Dumper=yaml.SafeDumper if key == "dbTimezones" else ExtraNewLinesDumper,
|
Dumper=yaml.SafeDumper if key == "dbTimezones" else ExtraNewLinesDumper,
|
||||||
)
|
)
|
||||||
assert formatted == snapshot_yaml
|
assert formatted == snapshot_yaml
|
||||||
|
|
||||||
|
|
||||||
|
def test_jwt_aud_config_set_to_invalid_uri(defaultenv):
|
||||||
|
"PostgREST should exit with an error message in output if jwt-aud config is set to an invalid URI"
|
||||||
|
env = {
|
||||||
|
**defaultenv,
|
||||||
|
"PGRST_JWT_AUD": "foo://%%$$^^.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
with pytest.raises(PostgrestError):
|
||||||
|
dump = cli(["--dump-config"], env=env).split("\n")
|
||||||
|
assert "jwt-aud should be a string or a valid URI" in dump
|
||||||
|
|||||||
+38
-9
@@ -93,26 +93,29 @@ def test_jwt_errors(defaultenv):
|
|||||||
response = postgrest.session.get("/", headers=headers)
|
response = postgrest.session.get("/", headers=headers)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
assert response.json()["message"] == "No suitable key or wrong key type"
|
assert response.json()["message"] == "No suitable key or wrong key type"
|
||||||
|
assert (
|
||||||
|
response.json()["details"] == "None of the keys was able to decode the JWT"
|
||||||
|
)
|
||||||
|
|
||||||
headers = jwtauthheader({"role": "not_existing"}, SECRET)
|
headers = jwtauthheader({"role": "not_existing"}, SECRET)
|
||||||
response = postgrest.session.get("/", headers=headers)
|
response = postgrest.session.get("/", headers=headers)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
assert response.json()["message"] == 'role "not_existing" does not exist'
|
assert response.json()["message"] == 'role "not_existing" does not exist'
|
||||||
|
|
||||||
# -31 seconds, because we allow clock skew of 30 seconds
|
# -35 seconds, because we allow clock skew of 30 seconds
|
||||||
headers = jwtauthheader({"exp": relativeSeconds(-31)}, SECRET)
|
headers = jwtauthheader({"exp": relativeSeconds(-35)}, SECRET)
|
||||||
response = postgrest.session.get("/", headers=headers)
|
response = postgrest.session.get("/", headers=headers)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
assert response.json()["message"] == "JWT expired"
|
assert response.json()["message"] == "JWT expired"
|
||||||
|
|
||||||
# 31 seconds, because we allow clock skew of 30 seconds
|
# 35 seconds, because we allow clock skew of 30 seconds
|
||||||
headers = jwtauthheader({"nbf": relativeSeconds(31)}, SECRET)
|
headers = jwtauthheader({"nbf": relativeSeconds(35)}, SECRET)
|
||||||
response = postgrest.session.get("/", headers=headers)
|
response = postgrest.session.get("/", headers=headers)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
assert response.json()["message"] == "JWT not yet valid"
|
assert response.json()["message"] == "JWT not yet valid"
|
||||||
|
|
||||||
# 31 seconds, because we allow clock skew of 30 seconds
|
# 35 seconds, because we allow clock skew of 35 seconds
|
||||||
headers = jwtauthheader({"iat": relativeSeconds(31)}, SECRET)
|
headers = jwtauthheader({"iat": relativeSeconds(35)}, SECRET)
|
||||||
response = postgrest.session.get("/", headers=headers)
|
response = postgrest.session.get("/", headers=headers)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
assert response.json()["message"] == "JWT issued at future"
|
assert response.json()["message"] == "JWT issued at future"
|
||||||
@@ -141,6 +144,10 @@ def test_jwt_errors(defaultenv):
|
|||||||
response = postgrest.session.get("/", headers=headers)
|
response = postgrest.session.get("/", headers=headers)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
assert response.json()["message"] == "Wrong or unsupported encoding algorithm"
|
assert response.json()["message"] == "Wrong or unsupported encoding algorithm"
|
||||||
|
assert (
|
||||||
|
response.json()["details"]
|
||||||
|
== "JWT is unsecured but expected 'alg' was not 'none'"
|
||||||
|
)
|
||||||
|
|
||||||
env = {
|
env = {
|
||||||
**defaultenv,
|
**defaultenv,
|
||||||
@@ -1355,9 +1362,9 @@ def test_log_postgrest_version(defaultenv):
|
|||||||
assert "Starting PostgREST %s..." % version in output[0]
|
assert "Starting PostgREST %s..." % version in output[0]
|
||||||
|
|
||||||
|
|
||||||
def test_log_postgrest_host_and_port(defaultenv):
|
@pytest.mark.parametrize("host", ["127.0.0.1", "::1"])
|
||||||
|
def test_log_postgrest_host_and_port(host, defaultenv):
|
||||||
"PostgREST should output the host and port it is bound to."
|
"PostgREST should output the host and port it is bound to."
|
||||||
host = "127.0.0.1"
|
|
||||||
port = freeport()
|
port = freeport()
|
||||||
|
|
||||||
with run(
|
with run(
|
||||||
@@ -1365,7 +1372,10 @@ def test_log_postgrest_host_and_port(defaultenv):
|
|||||||
) as postgrest:
|
) as postgrest:
|
||||||
output = postgrest.read_stdout(nlines=10)
|
output = postgrest.read_stdout(nlines=10)
|
||||||
|
|
||||||
assert f"API server listening on {host}:{port}" in output[2] # output-sensitive
|
if is_ipv6(host): # IPv6
|
||||||
|
assert f"API server listening on [{host}]:{port}" in output[2]
|
||||||
|
else: # IPv4
|
||||||
|
assert f"API server listening on {host}:{port}" in output[2]
|
||||||
|
|
||||||
|
|
||||||
def test_succeed_w_role_having_superuser_settings(defaultenv):
|
def test_succeed_w_role_having_superuser_settings(defaultenv):
|
||||||
@@ -1723,6 +1733,7 @@ def test_admin_metrics(defaultenv):
|
|||||||
with run(env=defaultenv, port=freeport()) as postgrest:
|
with run(env=defaultenv, port=freeport()) as postgrest:
|
||||||
response = postgrest.admin.get("/metrics")
|
response = postgrest.admin.get("/metrics")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
assert response.headers["Content-Type"] == "text/plain; charset=utf-8"
|
||||||
assert "pgrst_schema_cache_query_time_seconds" in response.text
|
assert "pgrst_schema_cache_query_time_seconds" in response.text
|
||||||
assert 'pgrst_schema_cache_loads_total{status="SUCCESS"}' in response.text
|
assert 'pgrst_schema_cache_loads_total{status="SUCCESS"}' in response.text
|
||||||
assert "pgrst_db_pool_max" in response.text
|
assert "pgrst_db_pool_max" in response.text
|
||||||
@@ -1929,3 +1940,21 @@ def test_schema_cache_error_observation(defaultenv):
|
|||||||
"Failed to load the schema cache using db-schemas=public and db-extra-search-path=x"
|
"Failed to load the schema cache using db-schemas=public and db-extra-search-path=x"
|
||||||
in output[7]
|
in output[7]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_listener_connection_errors(defaultenv):
|
||||||
|
"The logs should show the listener connection error message in a single line"
|
||||||
|
|
||||||
|
env = {
|
||||||
|
**defaultenv,
|
||||||
|
"PGHOST": "no_host",
|
||||||
|
"PGRST_DB_CHANNEL_ENABLED": "true",
|
||||||
|
}
|
||||||
|
|
||||||
|
with run(env=env, no_startup_stdout=False, wait_for_readiness=False) as postgrest:
|
||||||
|
output = postgrest.read_stdout(nlines=5)
|
||||||
|
assert any(
|
||||||
|
'Failed listening for database notifications on the "pgrst" channel. could not translate host name "no_host" to address:'
|
||||||
|
in line
|
||||||
|
for line in output
|
||||||
|
)
|
||||||
|
|||||||
@@ -1058,6 +1058,40 @@ spec = describe "OpenAPI" $ do
|
|||||||
}
|
}
|
||||||
|]
|
|]
|
||||||
|
|
||||||
|
it "only includes POST method for volatile functions" $ do
|
||||||
|
r <- simpleBody <$> get "/"
|
||||||
|
let volatileGet = r ^? key "paths" . key "/rpc/reset_table" . key "get"
|
||||||
|
volatilePost = r ^? key "paths" . key "/rpc/reset_table" . key "post"
|
||||||
|
|
||||||
|
liftIO $ do
|
||||||
|
volatileGet `shouldBe` Nothing
|
||||||
|
volatilePost `shouldNotBe` Nothing
|
||||||
|
|
||||||
|
it "includes GET and POST methods for stable functions" $ do
|
||||||
|
r <- simpleBody <$> get "/"
|
||||||
|
let stableGet = r ^? key "paths" . key "/rpc/getallusers" . key "get"
|
||||||
|
stablePost = r ^? key "paths" . key "/rpc/getallusers" . key "post"
|
||||||
|
|
||||||
|
liftIO $ do
|
||||||
|
stableGet `shouldNotBe` Nothing
|
||||||
|
stablePost `shouldNotBe` Nothing
|
||||||
|
|
||||||
|
it "includes GET and POST methods for immutable functions" $ do
|
||||||
|
r <- simpleBody <$> get "/"
|
||||||
|
let immutableGet = r ^? key "paths" . key "/rpc/jwt_test" . key "get"
|
||||||
|
immutablePost = r ^? key "paths" . key "/rpc/jwt_test" . key "post"
|
||||||
|
|
||||||
|
liftIO $ do
|
||||||
|
immutableGet `shouldNotBe` Nothing
|
||||||
|
immutablePost `shouldNotBe` Nothing
|
||||||
|
|
||||||
|
it "does not include empty enum in the preferParams parameter" $ do
|
||||||
|
r <- simpleBody <$> get "/"
|
||||||
|
let preferParams = r ^? key "parameters" . key "preferParams" . key "enum"
|
||||||
|
|
||||||
|
liftIO $ do
|
||||||
|
preferParams `shouldBe` Nothing
|
||||||
|
|
||||||
describe "Security" $
|
describe "Security" $
|
||||||
it "does not include security or security definitions by default" $ do
|
it "does not include security or security definitions by default" $ do
|
||||||
r <- simpleBody <$> get "/"
|
r <- simpleBody <$> get "/"
|
||||||
|
|||||||
@@ -72,9 +72,9 @@ spec =
|
|||||||
, matchHeaders = []
|
, matchHeaders = []
|
||||||
}
|
}
|
||||||
|
|
||||||
it "fails trying to read table from unkown schema" $
|
it "fails trying to read table from unknown schema" $
|
||||||
request methodGet "/parents" [("Accept-Profile", "unkown")] "" `shouldRespondWith`
|
request methodGet "/parents" [("Accept-Profile", "unknown")] "" `shouldRespondWith`
|
||||||
[json|{"message":"The schema must be one of the following: v1, v2, SPECIAL \"@/\\#~_-","code":"PGRST106","details":null,"hint":null}|]
|
[json|{"message":"Invalid schema: unknown","code":"PGRST106","details":null,"hint":"Only the following schemas are exposed: v1, v2, SPECIAL \"@/\\#~_-"}|]
|
||||||
{
|
{
|
||||||
matchStatus = 406
|
matchStatus = 406
|
||||||
}
|
}
|
||||||
@@ -151,7 +151,7 @@ spec =
|
|||||||
request methodPost "/children" [("Content-Profile", "unknown")]
|
request methodPost "/children" [("Content-Profile", "unknown")]
|
||||||
[json|{"name": "child 4", "parent_id": 4}|]
|
[json|{"name": "child 4", "parent_id": 4}|]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"message":"The schema must be one of the following: v1, v2, SPECIAL \"@/\\#~_-","code":"PGRST106","details":null,"hint":null}|]
|
[json|{"message":"Invalid schema: unknown","code":"PGRST106","details":null,"hint":"Only the following schemas are exposed: v1, v2, SPECIAL \"@/\\#~_-"}|]
|
||||||
{
|
{
|
||||||
matchStatus = 406
|
matchStatus = 406
|
||||||
}
|
}
|
||||||
@@ -389,9 +389,9 @@ spec =
|
|||||||
let def = simpleBody r ^? key "definitions" . key "another_table"
|
let def = simpleBody r ^? key "definitions" . key "another_table"
|
||||||
def `shouldBe` Nothing
|
def `shouldBe` Nothing
|
||||||
|
|
||||||
it "fails trying to read definitions from unkown schema" $
|
it "fails trying to read definitions from unknown schema" $
|
||||||
request methodGet "/" [("Accept-Profile", "unkown")] "" `shouldRespondWith`
|
request methodGet "/" [("Accept-Profile", "unknown")] "" `shouldRespondWith`
|
||||||
[json|{"message":"The schema must be one of the following: v1, v2, SPECIAL \"@/\\#~_-","code":"PGRST106","details":null,"hint":null}|]
|
[json|{"message":"Invalid schema: unknown","code":"PGRST106","details":null,"hint":"Only the following schemas are exposed: v1, v2, SPECIAL \"@/\\#~_-"}|]
|
||||||
{
|
{
|
||||||
matchStatus = 406
|
matchStatus = 406
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -294,6 +294,20 @@ spec = do
|
|||||||
]|]
|
]|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "works when the column type is a tsvector domain" $ do
|
||||||
|
get "tsearch_to_tsvector?select=text_search_domain&text_search_domain=fts(simple).of" `shouldRespondWith`
|
||||||
|
[json| [
|
||||||
|
{"text_search_domain":"'do':7 'fun':5 'impossible':9 'it':1 'kind':3 'of':4 's':2 'the':8 'to':6"}
|
||||||
|
]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "works when the column type is a recursive tsvector domain" $ do
|
||||||
|
get "tsearch_to_tsvector?select=text_search_rec_domain&text_search_rec_domain=fts(simple).of" `shouldRespondWith`
|
||||||
|
[json| [
|
||||||
|
{"text_search_rec_domain":"'do':7 'fun':5 'impossible':9 'it':1 'kind':3 'of':4 's':2 'the':8 'to':6"}
|
||||||
|
]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
context "text and json columns" $ do
|
context "text and json columns" $ do
|
||||||
it "finds matches with to_tsquery" $ do
|
it "finds matches with to_tsquery" $ do
|
||||||
get "/tsearch_to_tsvector?select=text_search&text_search=fts.impossible" `shouldRespondWith`
|
get "/tsearch_to_tsvector?select=text_search&text_search=fts.impossible" `shouldRespondWith`
|
||||||
|
|||||||
@@ -998,6 +998,22 @@ spec =
|
|||||||
|]
|
|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "should work with filters that use the fts operator when the column type is a tsvector domain" $
|
||||||
|
get "/rpc/get_tsearch_to_tsvector?select=text_search_domain&text_search_domain=fts(simple).impossible" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"text_search_domain":"'do':7 'fun':5 'impossible':9 'it':1 'kind':3 'of':4 's':2 'the':8 'to':6"},
|
||||||
|
{"text_search_domain":"'amusant':5 'c':1 'de':6 'est':2 'faire':7 'impossible':9 'l':8 'peu':4 'un':3"}]
|
||||||
|
|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "should work with filters that use the fts operator when the column type is a recursive tsvector domain" $
|
||||||
|
get "/rpc/get_tsearch_to_tsvector?select=text_search_rec_domain&text_search_rec_domain=fts(simple).impossible" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"text_search_rec_domain":"'do':7 'fun':5 'impossible':9 'it':1 'kind':3 'of':4 's':2 'the':8 'to':6"},
|
||||||
|
{"text_search_rec_domain":"'amusant':5 'c':1 'de':6 'est':2 'faire':7 'impossible':9 'l':8 'peu':4 'un':3"}]
|
||||||
|
|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "should work with the phraseto_tsquery function" $
|
it "should work with the phraseto_tsquery function" $
|
||||||
get "/rpc/get_tsearch?text_search_vector=phfts(english).impossible" `shouldRespondWith`
|
get "/rpc/get_tsearch?text_search_vector=phfts(english).impossible" `shouldRespondWith`
|
||||||
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
|
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
|
||||||
|
|||||||
@@ -600,3 +600,37 @@ spec =
|
|||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [matchContentTypeJson]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context "empty spreads embeds" $
|
||||||
|
it "should work return the same as empty embeddings" $ do
|
||||||
|
get "/actors?select=*,...films()"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| [{"id":1,"name":"john"}, {"id":2,"name":"mary"}] |]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
|
get "/grandchild_entities?select=name,...child_entities(parent_name:name,...entities())"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"name":"grandchild entity 1","parent_name":"child entity 1"},
|
||||||
|
{"name":"grandchild entity 2","parent_name":"child entity 1"},
|
||||||
|
{"name":"grandchild entity 3","parent_name":"child entity 2"},
|
||||||
|
{"name":"(grandchild,entity,4)","parent_name":"child entity 2"},
|
||||||
|
{"name":"(grandchild,entity,5)","parent_name":"child entity 2"}]
|
||||||
|
|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
|
get "/factories?select=factory:name,...processes()"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"factory":"Factory A"},
|
||||||
|
{"factory":"Factory B"},
|
||||||
|
{"factory":"Factory C"},
|
||||||
|
{"factory":"Factory D"}]
|
||||||
|
|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+2
-1
@@ -954,7 +954,8 @@ INSERT INTO tsearch_to_tsvector(text_search) VALUES ('C''est un peu amusant de f
|
|||||||
INSERT INTO tsearch_to_tsvector(text_search) VALUES ('Es ist eine Art Spaß, das Unmögliche zu machen');
|
INSERT INTO tsearch_to_tsvector(text_search) VALUES ('Es ist eine Art Spaß, das Unmögliche zu machen');
|
||||||
|
|
||||||
UPDATE tsearch_to_tsvector SET jsonb_search = jsonb_build_object('text_search', text_search);
|
UPDATE tsearch_to_tsvector SET jsonb_search = jsonb_build_object('text_search', text_search);
|
||||||
|
UPDATE tsearch_to_tsvector SET text_search_domain = to_tsvector('simple', text_search);
|
||||||
|
UPDATE tsearch_to_tsvector SET text_search_rec_domain = to_tsvector('simple', text_search);
|
||||||
|
|
||||||
TRUNCATE TABLE artists CASCADE;
|
TRUNCATE TABLE artists CASCADE;
|
||||||
INSERT INTO artists
|
INSERT INTO artists
|
||||||
|
|||||||
Vendored
+9
-1
@@ -3750,9 +3750,17 @@ create table surr_gen_default_upsert (
|
|||||||
extra text
|
extra text
|
||||||
);
|
);
|
||||||
|
|
||||||
|
create domain tsvector_not_null as tsvector
|
||||||
|
constraint "tsvector is required" check (value is not null);
|
||||||
|
|
||||||
|
create domain tsvector_not_empty as tsvector_not_null
|
||||||
|
constraint "tsvector is required and not empty" check (value <> '');
|
||||||
|
|
||||||
create table tsearch_to_tsvector (
|
create table tsearch_to_tsvector (
|
||||||
text_search text,
|
text_search text,
|
||||||
jsonb_search jsonb
|
jsonb_search jsonb,
|
||||||
|
text_search_domain tsvector_not_null default '',
|
||||||
|
text_search_rec_domain tsvector_not_empty default '.'
|
||||||
);
|
);
|
||||||
|
|
||||||
create function test.get_tsearch_to_tsvector() returns setof test.tsearch_to_tsvector AS $$
|
create function test.get_tsearch_to_tsvector() returns setof test.tsearch_to_tsvector AS $$
|
||||||
|
|||||||
Reference in New Issue
Block a user