Compare commits

...
45 Commits
Author SHA1 Message Date
steve-chavez e30bc63f49 bump version to 13.0.7 2025-09-14 14:08:53 -05:00
Taimoor ZaeemandSteve Chavez a8f40c4908 feat: improve error details of PGRST301 error 2025-09-14 13:37:17 -05:00
Taimoor ZaeemandSteve Chavez 75972e9ffe refactor: move jwt error messages to Error.hs module 2025-09-14 13:37:17 -05:00
Laurence IslaandSteve Chavez 4ba6b1b30c feat: improve error response when the requested schema is invalid
- It now shows the invalid schema in the "message"
- The exposed schemas are now listed in the "hint" instead of the "message"
2025-09-14 13:37:17 -05:00
renovate[bot]andWolfgang Walther a4927141ee chore(deps): update codecov/codecov-action action to v5.5.1 2025-09-04 19:25:55 +00:00
renovate[bot]andWolfgang Walther 1b353590ff chore(deps): update ubuntu:noble docker digest to 9cbed75 2025-09-03 12:50:39 +00:00
renovate[bot]andWolfgang Walther 394aa026d3 chore(deps): update ubuntu:noble docker digest to f3b7f1b 2025-09-02 08:18:24 +00:00
steve-chavez 272e2e7535 bump version to 13.0.6 2025-08-30 13:55:34 -05:00
Laurence IslaandWolfgang Walther ea153523d1 fix: empty enum in 'preferParams' openAPI parameter 2025-08-30 08:31:21 +02:00
Taimoor ZaeemandWolfgang Walther bf0a1173b5 fix: format of IPv6 address logged at PostgREST startup
The IPv6 address logged at the startup like `::1:80` was
wrong because the port isn't clearly separated. This commit
corrects it, now logging as `[::1]:80`.

This is done in accordance to RFC 3986. In short, we did this
have a clear separation between the port and host because
the components of an IPv6 are separated with the ':' character.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-08-29 22:31:01 +02:00
renovate[bot]andWolfgang Walther 3a28968f3c chore(deps): update nixbuild/nix-quick-install-action action to v33 2025-08-25 15:08:25 +00:00
Laurence IslaandWolfgang Walther 86aac1ead5 fix: logging the Haskell type instead of the listener error message directly
Previously: Just "connection error..."
Now: connection error...
2025-08-25 10:25:48 +00:00
steve-chavez 1f1f40a3b9 bump version to 13.0.5 2025-08-24 12:29:22 -05:00
Taimoor ZaeemandWolfgang Walther 86b0f82b6c fix(admin): metrics endpoint not responding with Content-Type header
The prometheus metrics text format requires `Content-Type` header
for correct scraping which fails otherwise. Closes #4271.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-08-21 13:47:19 +00:00
Taimoor ZaeemandWolfgang Walther d863065a51 fix: empty spread embeddings return unexpected SQL error
Fixes the SQL error from postgres when an empty spread embeddings
like `...table()` is requested.
2025-08-21 13:47:19 +00:00
renovate[bot]andWolfgang Walther 6ecacbc4cc chore(deps): update codecov/codecov-action action to v5.5.0 2025-08-20 18:10:51 +00:00
renovate[bot]andWolfgang Walther beb7d4f0e0 chore(deps): update ubuntu:noble docker digest to 7c06e91 2025-08-13 07:19:03 +00:00
renovate[bot]andWolfgang Walther b01d08d42f chore(deps): update actions/cache action to v4.2.4 2025-08-11 15:32:45 +00:00
renovate[bot]andWolfgang Walther 8628cb7cdf chore(deps): update actions/checkout action to v5 2025-08-11 15:31:50 +00:00
renovate[bot]andWolfgang Walther 0f2470c79d chore(deps): update actions/download-artifact action to v5 2025-08-06 07:44:53 +00:00
renovate[bot]andWolfgang Walther dc4e225b20 chore(deps): update docker/login-action action to v3.5.0 2025-08-04 17:11:07 +00:00
renovate[bot]andWolfgang Walther d201c5cac3 chore(deps): update haskell-actions/setup action to v2.8.1 2025-08-03 16:27:21 +00:00
Taimoor ZaeemandWolfgang Walther 16d59e825c test: adjust jwt claims error test to avoid failure
The JWT claims IO test fails too often. This breaks our
workflows. This commit adjusts the edge cases in test to
prevent flakiness.
2025-07-19 14:22:51 +02:00
renovate[bot]andWolfgang Walther dba6bda214 chore(deps): update ubuntu:noble docker digest to a08e551 2025-07-19 12:17:00 +00:00
Laurence Isla 9897ad2d9c chore: update sponsor 2025-07-17 11:56:34 -05:00
renovate[bot]andWolfgang Walther f82a11df49 chore(deps): update ubuntu:noble docker digest to c4570d2 2025-07-16 09:05:25 +00:00
Wolfgang Walther a87833cc10 docs: adjust some redirects
Those were reported in the weekly linkcheck.
2025-07-16 11:04:34 +02:00
renovate[bot]andWolfgang Walther a64e6fe87f chore(deps): update ubuntu:noble docker digest to e356c06 2025-07-16 08:51:04 +00:00
renovate[bot]andWolfgang Walther be4662e96e chore(deps): update all dependencies 2025-07-09 07:09:10 +00:00
Wolfgang Walther 97f9898e97 chore: bump some hackage dependencies
All of these were tested via stackage 23.27 which required allow-newer
for them.
2025-07-08 22:02:07 +02:00
Wolfgang Walther 0b058d6934 chore: fix stack's nix integration
The pkgconfig package has been renamed to pkg-config in... February
2019. So this has not been working for quite some time.
2025-07-08 20:29:24 +02:00
Wolfgang Walther 70057e65a5 chore: remove left-over comment for Ubuntu (arm)
We have been building with GHC 9.6 on that platform for a while.
2025-07-08 20:18:18 +02:00
Wolfgang Walther 5030c665be chore: build with GHC 9.8.4 for cabal 2025-07-08 20:18:17 +02:00
Wolfgang Walther ac6dac64b8 chore: update hackage index-state 2025-07-08 20:18:16 +02:00
Wolfgang Walther 2f8dbdb183 chore: stackage 22.41 -> 22.44
Updates stackage to 22.44, which is also supported on FreeBSD, where GHC
9.6.7 is available again.
2025-07-08 20:18:15 +02:00
steve-chavezandWolfgang Walther 67379f1d5e docs: clarify health checks empty response body 2025-07-08 20:18:13 +02:00
Wolfgang Walther d30abd99ae chore: remove Paths_postgrest module
The dependency on it was removed in #3608 already, but we forgot to
remove it from postgrest.cabal, which caused it to still be built.

We didn't realize because all references of it were stripped away by
dead code elimination anyway.
2025-07-08 18:37:14 +02:00
Joel JakobssonandWolfgang Walther 73d335e976 fix(openapi): respect function volatility for GET methods
The OpenAPI specification was incorrectly exposing GET methods for
VOLATILE functions, even though such functions properly reject GET
requests at runtime with "405 Method Not Allowed".  This created
a mismatch between the advertised API specification and the actual
runtime behavior.

VOLATILE functions should only be callable via POST since they may
have side effects, while STABLE and IMMUTABLE functions can safely
be called via GET since they don't modify database state.

Fix by checking the pdVolatility field in makeProcPathItem() and
only including GET methods in the OpenAPI PathItem for non-volatile
functions.

The runtime behavior was already correct; this fixes only the
OpenAPI documentation generation.
2025-07-07 17:28:07 +02:00
Taimoor ZaeemandWolfgang Walther 81b25871ae docs: horizontal filtering on table-valued functions 2025-07-05 21:09:56 +02:00
Taimoor ZaeemandWolfgang Walther 8f4a200f33 fix: OpenAPI broken docs link 2025-06-28 07:58:14 +00:00
Wolfgang Walther 087b9ecca1 ci: fix loadtest job on stable branches
Previously, the loadtest would always compare against main and the
latest tag. This meant a loadtest running on the v13 release branch,
would compare to a *future* version of both. This is not useful, and
also not supported by `postgrest-loadtest-against`, which recently
introduced a change on `main`, that now breaks the tests on the back
branches.

With this change, the loadtest will always run against the current
target branch of a PR, so against the v13 branch for a PR against v13,
for example. Also, it will compare against the latest released tag *for
that branch*.

Thus, when running this for v12, it will test against the v12 branch and
the v12.2.12 tag.
2025-06-26 09:44:01 +00:00
Wolfgang Walther 59eaae10ca ci: update Nix to 2.29.1
Related:
https://discourse.nixos.org/t/security-advisory-privilege-escalations-in-nix-lix-and-guix/66017
2025-06-25 12:58:46 +02:00
Laurence Isla a34d37bb82 bump version to 13.0.4 2025-06-17 19:58:43 -05:00
Taimoor ZaeemandLaurence Isla 5b45113565 fix: jwt-aud config not failing when set to invalid URI (#4140)
The `jwt-aud` config was not validated when containing ':'
character according to RFC 3986. This fix validates it and
fails at startup if it is invalid.
2025-06-18 00:11:30 +00:00
Laurence Isla 733a896113 fix: regression that makes fts not work on domain types based on tsvector 2025-06-18 00:11:30 +00:00
49 changed files with 482 additions and 300 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
freebsd_instance:
image_family: freebsd-14-2
image_family: freebsd-14-3
build_task:
# 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 "{}" +
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
bin_artifacts:
+2 -2
View File
@@ -19,14 +19,14 @@ inputs:
runs:
using: composite
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/')) }}
with:
path: ${{ inputs.path }}
key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
restore-keys: |
${{ 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/')) }}
with:
path: ${{ inputs.path }}
+1 -1
View File
@@ -11,7 +11,7 @@ inputs:
runs:
using: composite
steps:
- uses: nixbuild/nix-quick-install-action@5bb6a3b3abe66fd09bbf250dce8ada94f856a703 # v30
- uses: nixbuild/nix-quick-install-action@1f095fee853b33114486cfdeae62fa099cda35a9 # v33
with:
nix_conf: |-
always-allow-substitutes = true
+9 -9
View File
@@ -33,7 +33,7 @@ jobs:
name: Nix - Linux x86-64 static
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -62,7 +62,7 @@ jobs:
name: Nix - MacOS
runs-on: macos-14
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -126,11 +126,11 @@ jobs:
name: Stack - ${{ matrix.name }}
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: haskell-actions/setup@64445b6b5dd545faf5f8e2acee8253eb5c2b29aa # v2.7.11
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: haskell-actions/setup@7909071ceec0344debcc968c6c7a96a52e8dd0d7 # v2.8.1
with:
# This must match the version in stack.yaml's resolver
ghc-version: 9.6.6
ghc-version: 9.6.7
enable-stack: true
stack-no-global: true
stack-setup-ghc: true
@@ -168,7 +168,7 @@ jobs:
name: Stack - FreeBSD from CirrusCI
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: ./.github/actions/artifact-from-cirrus
with:
token: ${{ github.token }}
@@ -180,13 +180,13 @@ jobs:
cabal:
strategy:
matrix:
ghc: ['9.6.6', '9.8.2']
ghc: ['9.6.7', '9.8.4']
fail-fast: false
name: Cabal - Linux x86-64 - GHC ${{ matrix.ghc }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: haskell-actions/setup@64445b6b5dd545faf5f8e2acee8253eb5c2b29aa # v2.7.11
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: haskell-actions/setup@7909071ceec0344debcc968c6c7a96a52e8dd0d7 # v2.8.1
with:
ghc-version: ${{ matrix.ghc }}
- name: Cache .cabal
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
name: Lint & Style
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
+1 -1
View File
@@ -50,7 +50,7 @@ jobs:
- test
- build
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ssh-key: ${{ secrets.POSTGREST_SSH_KEY }}
- name: Tag latest commit
+2 -2
View File
@@ -27,7 +27,7 @@ jobs:
name: Build
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -41,7 +41,7 @@ jobs:
name: Spellcheck
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
+1 -1
View File
@@ -8,7 +8,7 @@ jobs:
linkcheck:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
+9 -9
View File
@@ -26,7 +26,7 @@ jobs:
needs:
- build
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Check the version to be released
run: |
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
@@ -64,9 +64,9 @@ jobs:
needs:
- prepare
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Download all artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
path: artifacts
- name: Create release bundle with archives for all builds
@@ -138,17 +138,17 @@ jobs:
env:
DOCKER_REPO: ${{ vars.DOCKER_REPO }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Download x86-64 Docker image
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: postgrest-docker-x86-64
- name: Download aarch64 binary
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: postgrest-ubuntu-aarch64
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
username: ${{ vars.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
@@ -194,7 +194,7 @@ jobs:
vars.DOCKER_REPO && vars.DOCKER_USER &&
github.ref == 'refs/tags/devel'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4.0.2
with:
username: ${{ vars.DOCKER_USER }}
+14 -11
View File
@@ -39,7 +39,7 @@ jobs:
# https://github.com/actions/runner/issues/241#issuecomment-842566950
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -49,7 +49,7 @@ jobs:
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
run: postgrest-coverage
- name: Upload coverage to codecov
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with:
files: ./coverage/codecov.json
token: ${{ secrets.CODECOV_TOKEN }}
@@ -76,7 +76,7 @@ jobs:
# https://github.com/actions/runner/issues/241#issuecomment-842566950
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -100,7 +100,7 @@ jobs:
name: Memory
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -117,7 +117,7 @@ jobs:
name: Loadtest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
- name: Setup Nix Environment
@@ -125,13 +125,16 @@ jobs:
with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
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
env:
TARGET_BRANCH: ${{ github.base_ref || github.ref_name }}
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"
flake:
@@ -146,7 +149,7 @@ jobs:
name: Flake Check
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
- name: Setup Nix Environment
+9 -9
View File
@@ -18,16 +18,16 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
</a>
</td>
<td align="center" valign="middle">
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/code-build.png">
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/tembo.png">
</a>
</td>
</tr>
<tr></tr>
<tr>
<td align="center" valign="middle">
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/tembo.png">
<a href="https://www.euronodes.com/postgrest" target="_blank">
<img width="296px" src="static/euronodes.svg">
</a>
</td>
<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">
</a>
</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>
</tbody>
</table>
@@ -93,6 +88,11 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
<img width="222px" src="static/oblivious.jpg">
</a>
</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>
</tbody>
</table>
+33
View File
@@ -5,6 +5,39 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## 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
### Fixed
+1 -1
View File
@@ -2,7 +2,7 @@
# The x86-64 is a single-static-binary image built via Nix, see:
# nix/tools/docker/README.md
FROM ubuntu:noble@sha256:6015f66923d7afbc53558d7ccffd325d43b4e249f41a6e93eef074c9505d2233 AS postgrest
FROM ubuntu:noble@sha256:9cbed754112939e914291337b5e554b07ad7c392491dba6daf25eef1332a22e8 AS postgrest
RUN apt-get update -y \
&& apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \
+4 -9
View File
@@ -27,16 +27,16 @@ API than you are likely to write from scratch.
</a>
</td>
<td align="center" valign="middle">
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/code-build.png">
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/tembo.png">
</a>
</td>
</tr>
<tr></tr>
<tr>
<td align="center" valign="middle">
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/tembo.png">
<a href="https://www.euronodes.com/postgrest" target="_blank">
<img width="296px" src="static/euronodes.svg">
</a>
</td>
<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">
</a>
</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>
</tbody>
</table>
+1 -1
View File
@@ -1 +1 @@
index-state: hackage.haskell.org 2025-02-01T14:59:33Z
index-state: hackage.haskell.org 2025-07-08T15:50:10Z
+4 -9
View File
@@ -24,16 +24,16 @@ write from scratch.
</a>
</td>
<td align="center" valign="middle">
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/code-build.png">
<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/tembo.png">
</a>
</td>
</tr>
<tr></tr>
<tr>
<td align="center" valign="middle">
<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/tembo.png">
<a href="https://www.euronodes.com/postgrest" target="_blank">
<img width="296px" src="static/euronodes.svg">
</a>
</td>
<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">
</a>
</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>
</tbody>
</table>
+16 -26
View File
@@ -41,28 +41,28 @@ Sponsors
.. container:: img-dark
.. 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
.. image:: ../static/neon.jpg
:target: https://neon.tech/?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
|
:target: https://neon.com/?utm_source=sponsor&utm_campaign=postgrest
.. image:: ../static/tembo.png
: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
.. image:: ../static/supabase-dark.png
@@ -73,21 +73,11 @@ Sponsors
.. 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
.. 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
to create space and center the logos. It's not easy to layout with restructuredText.
.. .. image:: _static/empty.png
:target: #sponsors
.. image:: _static/empty.png
:target: #sponsors
|
+1
View File
@@ -174,6 +174,7 @@ unikernel
unix
updatable
unfulfillable
unselected
Untyped
UPSERT
Upsert
+1 -1
View File
@@ -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.
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::
+17
View File
@@ -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.
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
+6 -1
View File
@@ -128,6 +128,11 @@ The ``metrics`` endpoint on the :ref:`admin_server` endpoint provides metrics in
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
# TYPE pgrst_schema_cache_query_time_seconds gauge
pgrst_schema_cache_query_time_seconds 1.5937927e-2
@@ -246,7 +251,7 @@ See :ref:`proxy-status_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.
.. code-block:: bash
+14 -17
View File
@@ -1,5 +1,5 @@
name: postgrest
version: 13.0.3
version: 13.0.7
synopsis: REST API for any Postgres database
description: Reads the schema of a PostgreSQL database and creates RESTful routes
for tables, views, and functions, supporting all HTTP methods that security
@@ -16,15 +16,13 @@ extra-source-files: CHANGELOG.md
cabal-version: >= 1.10
tested-with:
-- stack on FreeBSD
GHC == 9.4.5
-- nix, cabal on Ubuntu (arm)
, GHC == 9.4.8
-- nix
GHC == 9.4.8
-- cabal on Ubuntu
-- stack on MacOS, Ubuntu, Windows
, GHC == 9.6.6
-- stack on FreeBSD, MacOS, Ubuntu, Windows
, GHC == 9.6.7
-- cabal on Ubuntu
, GHC == 9.8.2
, GHC == 9.8.4
source-repository head
type: git
@@ -91,12 +89,11 @@ library
PostgREST.Response.GucHeader
PostgREST.Response.Performance
PostgREST.Version
other-modules: Paths_postgrest
build-depends: base >= 4.9 && < 4.20
, HTTP >= 4000.3.7 && < 4000.5
, Ranged-sets >= 0.3 && < 0.5
, 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
, bytestring >= 0.10.8 && < 0.13
, cache >= 0.1.3 && < 0.2.0
@@ -105,7 +102,7 @@ library
, clock >= 0.8.3 && < 0.9.0
, configurator-pg >= 0.2 && < 0.3
, containers >= 0.5.7 && < 0.7
, cookie >= 0.4.2 && < 0.5
, cookie >= 0.4.2 && < 0.6
, directory >= 1.2.6 && < 1.4
, either >= 4.4.1 && < 5.1
, extra >= 1.7.0 && < 2.0
@@ -114,17 +111,17 @@ library
, hasql-dynamic-statements >= 0.3.1 && < 0.4
, hasql-notifications >= 0.2.2.2 && < 0.2.3
, 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
, http-types >= 0.12.2 && < 0.13
, insert-ordered-containers >= 0.2.2 && < 0.3
, iproute >= 1.7.0 && < 1.8
, jose-jwt >= 0.9.6 && < 0.11
, lens >= 4.14 && < 5.3
, lens >= 4.14 && < 5.4
, lens-aeson >= 1.0.1 && < 1.3
, mtl >= 2.2.2 && < 2.4
, neat-interpolation >= 0.5 && < 0.6
, network >= 2.6 && < 3.2
, network >= 2.6 && < 3.3
, network-uri >= 2.6.1 && < 2.8
, optparse-applicative >= 0.13 && < 0.19
, parsec >= 3.1.11 && < 3.2
@@ -152,7 +149,7 @@ library
-- for unix sockets; this is tested in test/io/test_io.py. See
-- https://github.com/kazu-yamamoto/logger/commit/3a71ca70afdbb93d4ecf0083eeba1fbbbcab3fc3
, 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,
-- see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304
-- -optP-Wno-nonportable-include-path
@@ -261,14 +258,14 @@ test-suite spec
, case-insensitive >= 1.2 && < 1.3
, containers >= 0.5.7 && < 0.7
, 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
, hspec >= 2.3 && < 2.12
, hspec-wai >= 0.10 && < 0.12
, hspec-wai-json >= 0.10 && < 0.12
, http-types >= 0.12.3 && < 0.13
, jose-jwt >= 0.9.6 && < 0.11
, lens >= 4.14 && < 5.3
, lens >= 4.14 && < 5.4
, lens-aeson >= 1.0.1 && < 1.3
, monad-control >= 1.0.1 && < 1.1
, postgrest
+2 -1
View File
@@ -16,6 +16,7 @@ import Network.Socket.ByteString
import PostgREST.AppState (AppState)
import PostgREST.Config (AppConfig (..))
import PostgREST.MediaType (MediaType (..), toContentType)
import PostgREST.Metrics (metricsToText)
import PostgREST.Network (resolveHost)
import PostgREST.Observation (Observation (..))
@@ -58,7 +59,7 @@ admin appState req respond = do
respond $ Wai.responseLBS HTTP.status200 [] (maybe mempty JSON.encode sCache)
["metrics"] -> do
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
+1 -1
View File
@@ -207,7 +207,7 @@ getAction resource schema method =
getSchema :: AppConfig -> RequestHeaders -> ByteString -> Either ApiRequestError (Schema, Bool)
getSchema AppConfig{configDbSchemas} hdrs method = do
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)
Nothing -> Right (defaultSchema, length configDbSchemas /= 1) -- if we have many schemas, assume the default schema was negotiated
where
+19 -18
View File
@@ -50,7 +50,8 @@ import PostgREST.Auth.JwtCache (lookupJwtCache)
import PostgREST.Auth.Types (AuthResult (..))
import PostgREST.Config (AppConfig (..), FilterExp (..),
JSPath, JSPathExp (..))
import PostgREST.Error (Error (..), JwtError (..))
import PostgREST.Error (Error (..), JwtClaimsError (..),
JwtDecodeError (..), JwtError (..))
import Protolude
@@ -58,7 +59,7 @@ import Protolude
-- JSON object of JWT claims.
parseToken :: AppConfig -> Maybe ByteString -> UTCTime -> ExceptT Error IO JSON.Value
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
secret <- liftEither . maybeToRight (JwtErr JwtSecretMissing) $ configJWKS
tknWith3Parts <- liftEither $ hasThreeParts tkn
@@ -69,33 +70,33 @@ parseToken AppConfig{..} (Just tkn) time = do
hasThreeParts :: ByteString -> Either Error ByteString
hasThreeParts token = case length $ BS.split (BS.c2w '.') token of
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
-- The only errors we can get from JWT.decode function are:
-- BadAlgorithm
-- KeyError
-- BadCrypto
jwtDecodeError (JWT.KeyError _) = JwtDecodeError "No suitable key or wrong key type"
jwtDecodeError (JWT.BadAlgorithm _) = JwtDecodeError "Wrong or unsupported encoding algorithm"
jwtDecodeError JWT.BadCrypto = JwtDecodeError "JWT cryptographic operation failed"
jwtDecodeError (JWT.KeyError m) = JwtDecodeErr $ KeyError m
jwtDecodeError (JWT.BadAlgorithm m) = JwtDecodeErr $ BadAlgorithm m
jwtDecodeError JWT.BadCrypto = JwtDecodeErr BadCrypto
-- 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.Jws (_, claims)) = case JSON.decodeStrict claims of
Just jclaims@(JSON.Object mclaims) ->
verifyClaim mclaims "exp" isValidExpClaim "JWT expired" >>
verifyClaim mclaims "nbf" isValidNbfClaim "JWT not yet valid" >>
verifyClaim mclaims "iat" isValidIatClaim "JWT issued at future" >>
verifyClaim mclaims "aud" isValidAudClaim "JWT not in audience" >>
verifyClaim mclaims "exp" isValidExpClaim JWTExpired >>
verifyClaim mclaims "nbf" isValidNbfClaim JWTNotYetValid >>
verifyClaim mclaims "iat" isValidIatClaim JWTIssuedAtFuture >>
verifyClaim mclaims "aud" isValidAudClaim JWTNotInAudience >>
return jclaims
_ -> Left $ JwtClaimsError "Parsing claims failed"
_ -> Left $ JwtClaimsErr ParsingClaimsFailed
-- 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
isValid <- maybe (Right True) func (KM.lookup claim mclaims)
unless isValid $ Left $ JwtClaimsError err
unless isValid $ Left $ JwtClaimsErr err
allowedSkewSeconds = 30 :: Int64
now = floor . nominalDiffTimeToSeconds $ utcTimeToPOSIXSeconds time
@@ -104,15 +105,15 @@ parseToken AppConfig{..} (Just tkn) time = do
isValidExpClaim :: JSON.Value -> Either JwtError Bool
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.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.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.Null = Right True -- {"aud": null} is valid for all audiences
@@ -120,7 +121,7 @@ parseToken AppConfig{..} (Just tkn) time = do
isValidAudClaim (JSON.Array arr)
| null arr = Right True -- {"aud": []} is valid for all audiences
| 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 =>
AppConfig -> JSON.Value -> ExceptT Error m AuthResult
+16 -2
View File
@@ -48,7 +48,7 @@ import Data.List.NonEmpty (fromList, toList)
import Data.Maybe (fromJust)
import Data.Scientific (floatingOrInteger)
import Jose.Jwk (Jwk, JwkSet)
import Network.URI (escapeURIString,
import Network.URI (escapeURIString, isURI,
isUnescapedInURIComponent)
import Numeric (readOct, showOct)
import System.Environment (getEnvironment)
@@ -281,7 +281,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
<*> (fromMaybe "postgresql://" <$> optString "db-uri")
<*> pure optPath
<*> pure Nothing
<*> optString "jwt-aud"
<*> optStringOrURI "jwt-aud"
<*> parseRoleClaimKey "jwt-role-claim-key" "role-claim-key"
<*> (fmap encodeUtf8 <$> optString "jwt-secret")
<*> (fromMaybe False <$> optWithAlias
@@ -407,6 +407,20 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
optStringEmptyable :: C.Key -> C.Parser C.Config (Maybe Text)
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 k = join <$> overrideFromDbOrEnvironment C.optional k coerceInt
+61 -16
View File
@@ -14,6 +14,8 @@ module PostgREST.Error
, PgError(..)
, Error(..)
, JwtError (..)
, JwtDecodeError(..)
, JwtClaimsError(..)
, errorPayload
, status
) where
@@ -86,7 +88,7 @@ data ApiRequestError
| QueryParamError QPError
| RelatedOrderNotToOne Text Text
| UnacceptableFilter Text
| UnacceptableSchema [Text]
| UnacceptableSchema Text [Text]
| UnsupportedMethod ByteString
| GucHeadersError
| GucStatusError
@@ -194,7 +196,7 @@ instance ErrorBody ApiRequestError where
message (InvalidBody errorMessage) = T.decodeUtf8 errorMessage
message (InvalidRange _) = "Requested range not satisfiable"
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 (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"
@@ -234,6 +236,7 @@ instance ErrorBody ApiRequestError where
-- HINT: Maybe JSON.Value
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 (UnacceptableSchema _ schemas) = Just $ JSON.String $ "Only the following schemas are exposed: " <> T.intercalate ", " schemas
hint _ = Nothing
@@ -647,10 +650,32 @@ data Error
deriving Show
data JwtError
= JwtDecodeError Text
= JwtDecodeErr JwtDecodeError
| JwtSecretMissing
| 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
instance PgrstError Error where
@@ -696,14 +721,14 @@ instance ErrorBody Error where
hint (PgErr err) = hint err
instance PgrstError JwtError where
status JwtDecodeError{} = HTTP.unauthorized401
status JwtDecodeErr{} = HTTP.unauthorized401
status JwtSecretMissing = HTTP.status500
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 (JwtClaimsError m) = [invalidTokenHeader m]
headers e@(JwtClaimsErr _) = [invalidTokenHeader $ message e]
headers _ = mempty
instance JSON.ToJSON JwtError where
@@ -711,16 +736,36 @@ instance JSON.ToJSON JwtError where
(code err) (message err) (details err) (hint err)
instance ErrorBody JwtError where
code JwtSecretMissing = "PGRST300"
code (JwtDecodeError _) = "PGRST301"
code JwtTokenRequired = "PGRST302"
code (JwtClaimsError _) = "PGRST303"
code JwtSecretMissing = "PGRST300"
code (JwtDecodeErr _) = "PGRST301"
code JwtTokenRequired = "PGRST302"
code (JwtClaimsErr _) = "PGRST303"
message JwtSecretMissing = "Server lacks JWT secret"
message (JwtDecodeError msg) = msg
message JwtTokenRequired = "Anonymous access is disabled"
message (JwtClaimsError msg) = msg
message JwtSecretMissing = "Server lacks JWT secret"
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 (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
hint _ = Nothing
+5 -1
View File
@@ -13,5 +13,9 @@ resolveHost sock = do
sn <- NS.getSocketName sock
case sn of
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
+10 -10
View File
@@ -108,11 +108,8 @@ observationMessage = \case
DBListenStart channel -> do
"Listening for database notifications on the " <> show channel <> " channel"
DBListenFail channel listenErr ->
"Failed listening for database notifications on the " <> show channel <> " channel. " <> (
case listenErr of
Left err -> show err
Right err -> showListenerError err
)
"Failed listening for database notifications on the " <> show channel <> " channel. " <>
either showListenerConnError showListenerException listenErr
DBListenRetry delay ->
"Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..."
DBListenerGotSCacheMsg channel ->
@@ -157,8 +154,11 @@ observationMessage = \case
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err
showListenerError :: Either SomeException () -> Text
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) =
let showOnSingleLine txt = T.intercalate " " $ T.filter (/= '\t') <$> T.lines txt in -- the errors from hasql-notifications come intercalated with "\t\n"
showOnSingleLine $ show e
showOnSingleLine txt = T.intercalate " " $ T.filter (/= '\t') <$> T.lines txt -- the errors from hasql-notifications come intercalated with "\t\n"
showListenerConnError :: SQL.ConnectionError -> Text
showListenerConnError = maybe "Connection error" (showOnSingleLine . T.decodeUtf8)
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
+8 -8
View File
@@ -278,7 +278,7 @@ data ResolverContext = ResolverContext
}
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 toTsV=
@@ -291,12 +291,12 @@ resolveTypeOrUnknown ResolverContext{..} (fn, jp) toTsV =
case res of
-- types that are already json/jsonb don't need to be converted with `to_jsonb` for using arrow operators `data->attr`
-- this prevents indexes not applying https://github.com/PostgREST/postgrest/issues/2594
cf@CoercibleField{cfIRType="json"} -> cf{cfJsonPath=jp, cfToJson=False}
cf@CoercibleField{cfIRType="jsonb"} -> cf{cfJsonPath=jp, cfToJson=False}
cf@CoercibleField{cfIRType="json"} -> cf{cfJsonPath=jp, cfToJson=False}
cf@CoercibleField{cfIRType="jsonb"} -> cf{cfJsonPath=jp, cfToJson=False}
-- 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
cf -> cf{cfJsonPath=jp, cfToJson=True}
cf -> cf{cfJsonPath=jp, cfToJson=True}
where
res = fromMaybe (unknownField fn jp) $ HM.lookup qi tables >>=
Just . (\t -> resolveTableFieldName t fn toTsV)
@@ -891,7 +891,7 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
-- where_ = [
-- CoercibleStmnt (
-- 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
-- }
-- )
@@ -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.
--
-- >>> 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.
--
@@ -926,7 +926,7 @@ addNullEmbedFilters (Node rp@ReadPlan{where_=curLogic} forest) = do
newNullFilters rPlans = \case
(CoercibleExpr b lOp 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
case (foundRP, opExpr) of
(Just ReadPlan{relAggAlias}, OpExpr b (Is IsNull)) -> Right $ CoercibleStmnt $ CoercibleFilterNullEmbed b relAggAlias
+2 -1
View File
@@ -44,13 +44,14 @@ data CoercibleField = CoercibleField
, cfToJson :: Bool
, 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.
, 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.
, 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.
} deriving (Eq, Show)
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.
data CoercibleLogicTree
+7 -7
View File
@@ -70,21 +70,21 @@ readPlanToQuery node@(Node ReadPlan{select,from=mainQi,fromAlias,where_=logicFor
getJoinSelects :: ReadPlanTree -> [SQL.Snippet]
getJoinSelects (Node ReadPlan{relSelect} _) =
mapMaybe relSelectToSnippet relSelect
join $ map relSelectToSnippet relSelect
where
relSelectToSnippet :: RelSelectField -> Maybe SQL.Snippet
relSelectToSnippet :: RelSelectField -> [SQL.Snippet]
relSelectToSnippet fld =
let aggAlias = pgFmtIdent $ rsAggAlias fld
in
case fld of
JsonEmbed{rsEmptyEmbed = True} ->
Nothing
[]
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} ->
Just $ "COALESCE( " <> aggAlias <> "." <> aggAlias <> ", '[]') AS " <> pgFmtIdent rsSelName
["COALESCE( " <> aggAlias <> "." <> aggAlias <> ", '[]') AS " <> pgFmtIdent rsSelName]
Spread{rsSpreadSel, rsAggAlias} ->
Just $ intercalateSnippet ", " (pgFmtSpreadSelectItem rsAggAlias <$> rsSpreadSel)
pgFmtSpreadSelectItem rsAggAlias <$> rsSpreadSel
getJoins :: ReadPlanTree -> [SQL.Snippet]
getJoins (Node _ []) = []
@@ -182,7 +182,7 @@ callPlanToQuery (FunctionCall qi params arguments returnsScalar returnsSetOfScal
KeyParams [] -> "FROM " <> callIt mempty
KeyParams prms -> case arguments of
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)
callIt :: SQL.Snippet -> SQL.Snippet
+7 -5
View File
@@ -30,7 +30,8 @@ import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
import PostgREST.SchemaCache.Relationship (Cardinality (..),
Relationship (..),
RelationshipsMap)
import PostgREST.SchemaCache.Routine (Routine (..),
import PostgREST.SchemaCache.Routine (FuncVolatility (..),
Routine (..),
RoutineParam (..))
import PostgREST.SchemaCache.Table (Column (..), Table (..),
TablesMap,
@@ -170,8 +171,9 @@ makePreferParam ts =
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
& in_ .~ ParamHeader
& type_ ?~ SwaggerString
& enum_ .~ JSON.decode (JSON.encode $ foldl (<>) [] (val <$> ts)))
& enum_ .~ if null enu then Nothing else JSON.decode (JSON.encode enu))
where
enu = foldl (<>) [] (val <$> ts)
val :: Text -> [Text]
val = \case
"count" -> ["count=none"]
@@ -355,9 +357,9 @@ makeProcPathItem pd = ("/rpc/" ++ toS (pdName pd), pe)
& parameters .~ makeProcGetParams (pdParams pd)
postOp = procOp
& parameters .~ makeProcPostParams pd
pe = (mempty :: PathItem)
& get ?~ getOp
& post ?~ postOp
pe = case pdVolatility pd of
Volatile -> (mempty :: PathItem) & post ?~ postOp
_ -> (mempty :: PathItem) & get ?~ getOp & post ?~ postOp
makeRootPathItem :: (FilePath, PathItem)
makeRootPathItem = ("/", p)
+1 -1
View File
@@ -26,7 +26,7 @@ prettyVersion =
docsVersion :: Text
docsVersion
| 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.
-24
View File
@@ -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
-47
View File
@@ -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
View File
@@ -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:
packages:
- pcre
- pkgconfig
- postgresql
- libpq
- pkg-config
- zlib
# disable pure by default so that the test environment can be passed
pure: false
@@ -14,4 +13,3 @@ extra-deps:
- hasql-pool-1.0.1
- jose-jwt-0.10.0
- postgresql-libpq-0.10.1.0
- hasql-notifications-0.2.2.2
+5 -12
View File
@@ -1,7 +1,7 @@
# 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
# https://docs.haskellstack.org/en/stable/topics/lock_files
packages:
- completed:
@@ -32,16 +32,9 @@ packages:
size: 1096
original:
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:
- completed:
sha256: 1e32b51d9082fdf6f3bd92accc9dfffd4ddaf406404427fb10bf76d2bc03cbbb
size: 720263
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/41.yaml
original: lts-22.41
sha256: 238fa745b64f91184f9aa518fe04bdde6552533d169b0da5256670df83a0f1a9
size: 721141
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/44.yaml
original: lts-22.44
+5 -6
View File
@@ -41,12 +41,11 @@ cli:
use_defaultenv: true
env:
PGRST_SERVER_UNIX_SOCKET_MODE: '778'
# TODO: Bug needs to be fixed
# - name: invalid jwt-aud
# expect: error
# use_defaultenv: true
# env:
# PGRST_JWT_AUD: 'htp:/@@localhorst.invalid'
- name: invalid jwt-aud
expect: error
use_defaultenv: true
env:
PGRST_JWT_AUD: 'http://%%localhorst.invalid'
- name: invalid log-level
expect: error
use_defaultenv: true
+13 -2
View File
@@ -96,7 +96,9 @@ def run(
if port:
env["PGRST_SERVER_PORT"] = str(port)
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:
socketfile = pathlib.Path(tmpdir) / "postgrest.sock"
env["PGRST_SERVER_UNIX_SOCKET"] = str(socketfile)
@@ -104,7 +106,8 @@ def run(
adminport = freeport(port)
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]
env["HPCTIXFILE"] = hpctixfile()
@@ -218,3 +221,11 @@ def sleep_pool_connection(url, seconds):
session.get(url + f"/rpc/sleep?seconds={seconds}", timeout=0.1)
except requests.exceptions.ReadTimeout:
pass
def is_ipv6(addr):
try:
socket.inet_pton(socket.AF_INET6, addr)
return True
except OSError:
return False
+12
View File
@@ -278,3 +278,15 @@ def test_schema_cache_snapshot(baseenv, key, snapshot_yaml):
Dumper=yaml.SafeDumper if key == "dbTimezones" else ExtraNewLinesDumper,
)
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
View File
@@ -93,26 +93,29 @@ def test_jwt_errors(defaultenv):
response = postgrest.session.get("/", headers=headers)
assert response.status_code == 401
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)
response = postgrest.session.get("/", headers=headers)
assert response.status_code == 401
assert response.json()["message"] == 'role "not_existing" does not exist'
# -31 seconds, because we allow clock skew of 30 seconds
headers = jwtauthheader({"exp": relativeSeconds(-31)}, SECRET)
# -35 seconds, because we allow clock skew of 30 seconds
headers = jwtauthheader({"exp": relativeSeconds(-35)}, SECRET)
response = postgrest.session.get("/", headers=headers)
assert response.status_code == 401
assert response.json()["message"] == "JWT expired"
# 31 seconds, because we allow clock skew of 30 seconds
headers = jwtauthheader({"nbf": relativeSeconds(31)}, SECRET)
# 35 seconds, because we allow clock skew of 30 seconds
headers = jwtauthheader({"nbf": relativeSeconds(35)}, SECRET)
response = postgrest.session.get("/", headers=headers)
assert response.status_code == 401
assert response.json()["message"] == "JWT not yet valid"
# 31 seconds, because we allow clock skew of 30 seconds
headers = jwtauthheader({"iat": relativeSeconds(31)}, SECRET)
# 35 seconds, because we allow clock skew of 35 seconds
headers = jwtauthheader({"iat": relativeSeconds(35)}, SECRET)
response = postgrest.session.get("/", headers=headers)
assert response.status_code == 401
assert response.json()["message"] == "JWT issued at future"
@@ -141,6 +144,10 @@ def test_jwt_errors(defaultenv):
response = postgrest.session.get("/", headers=headers)
assert response.status_code == 401
assert response.json()["message"] == "Wrong or unsupported encoding algorithm"
assert (
response.json()["details"]
== "JWT is unsecured but expected 'alg' was not 'none'"
)
env = {
**defaultenv,
@@ -1355,9 +1362,9 @@ def test_log_postgrest_version(defaultenv):
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."
host = "127.0.0.1"
port = freeport()
with run(
@@ -1365,7 +1372,10 @@ def test_log_postgrest_host_and_port(defaultenv):
) as postgrest:
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):
@@ -1723,6 +1733,7 @@ def test_admin_metrics(defaultenv):
with run(env=defaultenv, port=freeport()) as postgrest:
response = postgrest.admin.get("/metrics")
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_loads_total{status="SUCCESS"}' 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"
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
)
+34
View File
@@ -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" $
it "does not include security or security definitions by default" $ do
r <- simpleBody <$> get "/"
@@ -72,9 +72,9 @@ spec =
, matchHeaders = []
}
it "fails trying to read table from unkown schema" $
request methodGet "/parents" [("Accept-Profile", "unkown")] "" `shouldRespondWith`
[json|{"message":"The schema must be one of the following: v1, v2, SPECIAL \"@/\\#~_-","code":"PGRST106","details":null,"hint":null}|]
it "fails trying to read table from unknown schema" $
request methodGet "/parents" [("Accept-Profile", "unknown")] "" `shouldRespondWith`
[json|{"message":"Invalid schema: unknown","code":"PGRST106","details":null,"hint":"Only the following schemas are exposed: v1, v2, SPECIAL \"@/\\#~_-"}|]
{
matchStatus = 406
}
@@ -151,7 +151,7 @@ spec =
request methodPost "/children" [("Content-Profile", "unknown")]
[json|{"name": "child 4", "parent_id": 4}|]
`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
}
@@ -389,9 +389,9 @@ spec =
let def = simpleBody r ^? key "definitions" . key "another_table"
def `shouldBe` Nothing
it "fails trying to read definitions from unkown schema" $
request methodGet "/" [("Accept-Profile", "unkown")] "" `shouldRespondWith`
[json|{"message":"The schema must be one of the following: v1, v2, SPECIAL \"@/\\#~_-","code":"PGRST106","details":null,"hint":null}|]
it "fails trying to read definitions from unknown schema" $
request methodGet "/" [("Accept-Profile", "unknown")] "" `shouldRespondWith`
[json|{"message":"Invalid schema: unknown","code":"PGRST106","details":null,"hint":"Only the following schemas are exposed: v1, v2, SPECIAL \"@/\\#~_-"}|]
{
matchStatus = 406
}
+14
View File
@@ -294,6 +294,20 @@ spec = do
]|]
{ 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
it "finds matches with to_tsquery" $ do
get "/tsearch_to_tsvector?select=text_search&text_search=fts.impossible" `shouldRespondWith`
+16
View File
@@ -998,6 +998,22 @@ spec =
|]
{ 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" $
get "/rpc/get_tsearch?text_search_vector=phfts(english).impossible" `shouldRespondWith`
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
@@ -600,3 +600,37 @@ spec =
{ matchStatus = 200
, 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]
}
+2 -1
View File
@@ -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');
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;
INSERT INTO artists
+9 -1
View File
@@ -3750,9 +3750,17 @@ create table surr_gen_default_upsert (
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 (
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 $$