Compare commits

...
30 Commits
Author SHA1 Message Date
steve-chavez cd3cf9ed97 bump version to 12.2.12 2025-05-01 20:24:30 -05:00
steve-chavez 1f28efa9bd fix: don't enable admin server /config by default
This now requires setting `admin-server-config-enabled`.
2025-05-01 20:24:08 -05:00
steve-chavez 36eb72c2a0 bump version to 12.2.11 2025-04-21 17:08:00 -05:00
Taimoor ZaeemandSteve Chavez 38c596800a fix: regression with parameter charset=utf-8 in mediatype 2025-04-21 17:06:26 -05:00
steve-chavez a7f9181462 bump version to 12.2.10 2025-04-18 21:28:00 -05:00
Michal KleczekandSteve Chavez f68d5944e6 fix: purge JWT cache asynchronously in a separate thread
Otherwise performance was reduced unnecessarily.
2025-04-18 21:27:36 -05:00
Wolfgang Walther b454f29b2c bump version to 12.2.9 2025-04-16 20:39:19 +02:00
Taimoor ZaeemandWolfgang Walther a3cc762f17 fix: valid JWTs after jwt-secret is changed in a config reload 2025-04-16 18:26:42 +00:00
Taimoor ZaeemandWolfgang Walther 66cd4ca596 fix: regression that replaces an unknown media type with */* (#4013) 2025-04-14 18:00:21 +02:00
Taimoor ZaeemandWolfgang Walther 513704dc2b fix: parsing of the for parameter of plan media type (#4005) 2025-04-12 15:37:37 +00:00
Thilo HohltandWolfgang Walther 026d84a093 docs: add archtika to example apps section on ecosystem page (#4007) 2025-04-11 14:34:56 +02:00
Taimoor ZaeemandWolfgang Walther 106f193a56 docs: add note that ordering of columns is not enforced (#3999) 2025-04-10 14:44:03 +02:00
steve-chavezandWolfgang Walther 79fc43ec39 docs: redirect from broken #bulk-insert-default 2025-04-04 18:24:26 +02:00
Taimoor ZaeemandWolfgang Walther b187f09574 docs: mention that updates also supports specifying columns and missing pref 2025-04-04 18:24:25 +02:00
Taimoor ZaeemandWolfgang Walther 4f1dc72e40 docs: explain missing preference header 2025-04-04 18:24:23 +02:00
renovate[bot]andWolfgang Walther d70d5cb19d chore(deps): update peter-evans/dockerhub-description action to v4.0.2 2025-04-03 12:19:48 +00:00
renovate[bot]andWolfgang Walther 6e851cce59 chore(deps): update peter-evans/dockerhub-description action to v4.0.1 2025-04-01 20:25:14 +00:00
renovate[bot]andWolfgang Walther accf46d2ee chore(deps): update all dependencies 2025-03-27 19:51:24 +00:00
renovate[bot]andWolfgang Walther 852f833d56 chore(deps): update all dependencies 2025-03-27 19:32:19 +00:00
Wolfgang Walther 48e6267dde docs: Remove broken link 2025-03-26 17:00:31 +01:00
Wolfgang Walther ed86bbf3de docs: remove broken link 2025-03-14 23:21:41 +01:00
Wolfgang Walther 3ba4290ef0 chore: Fix style check 2025-02-22 17:01:27 +01:00
Wolfgang Walther 0be047a52f docs: Fix outdated links 2025-02-22 16:10:50 +01:00
Wolfgang Walther d2d3367046 ci: Fix cirrus FreeBSD builds
Apparently Cirrus removed the 14-1 image. When I firsted looked into
this some days ago, when the job started failing, the docs were not
updated, yet - so it wasn't clear. Now the docs mention freebsd-14-2
explicitly...
2025-02-22 14:22:29 +01:00
Laurence IslaandWolfgang Walther ba6f985dac docs: make the aggregate functions docs less verbose 2025-02-22 13:19:10 +01:00
steve-chavezandWolfgang Walther 7cb007567b docs: reduce verbosity of aggregate functions 2025-02-22 13:19:10 +01:00
Taimoor ZaeemandWolfgang Walther acb72b7c67 docs: add missing jwt claims and clock skew (#3908) 2025-02-22 13:19:10 +01:00
steve-chavezandWolfgang Walther 8649d27709 docs: correct package for installation under Nix 2025-02-22 13:19:10 +01:00
steve-chavez 4a3936f71c bump version to 12.2.8 2025-02-10 13:25:22 -05:00
M. Taimoor Zaeemandsteve-chavez 9b6b5d06ea fix: log 503 client error to stderr 2025-02-10 13:24:52 -05:00
44 changed files with 469 additions and 239 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
freebsd_instance: freebsd_instance:
image_family: freebsd-14-1 image_family: freebsd-14-2
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
@@ -112,7 +112,7 @@ runs:
echo "artifacts=${artifacts}" >> "$GITHUB_OUTPUT" echo "artifacts=${artifacts}" >> "$GITHUB_OUTPUT"
- name: Save artifact to GitHub Actions - name: Save artifact to GitHub Actions
if: steps.find-task.outputs.task_found if: steps.find-task.outputs.task_found
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: ${{ inputs.upload }} name: ${{ inputs.upload }}
path: ${{ steps.download.outputs.artifacts }} path: ${{ steps.download.outputs.artifacts }}
+2 -2
View File
@@ -19,14 +19,14 @@ inputs:
runs: runs:
using: composite using: composite
steps: steps:
- uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
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@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 - uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
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 }}
+2 -2
View File
@@ -11,12 +11,12 @@ inputs:
runs: runs:
using: composite using: composite
steps: steps:
- uses: nixbuild/nix-quick-install-action@25aff27c252e0c8cdda3264805f7b6bcd92c8718 # v29 - uses: nixbuild/nix-quick-install-action@5bb6a3b3abe66fd09bbf250dce8ada94f856a703 # v30
with: with:
nix_conf: |- nix_conf: |-
always-allow-substitutes = true always-allow-substitutes = true
max-jobs = auto max-jobs = auto
- uses: cachix/cachix-action@ad2ddac53f961de1989924296a1f236fcfbaa4fc # v15 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: postgrest name: postgrest
authToken: ${{ inputs.authToken }} authToken: ${{ inputs.authToken }}
+5 -5
View File
@@ -42,7 +42,7 @@ jobs:
- name: Build static executable - name: Build static executable
run: nix-build -A postgrestStatic run: nix-build -A postgrestStatic
- name: Save built executable as artifact - name: Save built executable as artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: postgrest-linux-static-x86-64 name: postgrest-linux-static-x86-64
path: result/bin/postgrest path: result/bin/postgrest
@@ -51,7 +51,7 @@ jobs:
- name: Build Docker image - name: Build Docker image
run: nix-build -A docker.image --out-link postgrest-docker.tar.gz run: nix-build -A docker.image --out-link postgrest-docker.tar.gz
- name: Save built Docker image as artifact - name: Save built Docker image as artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: postgrest-docker-x86-64 name: postgrest-docker-x86-64
path: postgrest-docker.tar.gz path: postgrest-docker.tar.gz
@@ -118,7 +118,7 @@ jobs:
runs-on: ${{ matrix.runs-on }} runs-on: ${{ matrix.runs-on }}
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: haskell-actions/setup@bbd90a29996ac33b1c644a42206e312fc0379748 # v2.7.9 - uses: haskell-actions/setup@d9b5b3fcf7ca56b8fe585c9b77d3b0ce466affd2 # v2.7.10
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.5 ghc-version: 9.6.5
@@ -146,7 +146,7 @@ jobs:
- name: Strip Executable - name: Strip Executable
run: strip result/postgrest* run: strip result/postgrest*
- name: Save built executable as artifact - name: Save built executable as artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: ${{ matrix.artifact }} name: ${{ matrix.artifact }}
path: | path: |
@@ -177,7 +177,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: haskell-actions/setup@bbd90a29996ac33b1c644a42206e312fc0379748 # v2.7.9 - uses: haskell-actions/setup@d9b5b3fcf7ca56b8fe585c9b77d3b0ce466affd2 # v2.7.10
with: with:
ghc-version: ${{ matrix.ghc }} ghc-version: ${{ matrix.ghc }}
- name: Cache .cabal - name: Cache .cabal
+8 -8
View File
@@ -49,7 +49,7 @@ jobs:
echo "Relevant extract from CHANGELOG.md:" echo "Relevant extract from CHANGELOG.md:"
cat CHANGES.md cat CHANGES.md
- name: Save CHANGES.md as artifact - name: Save CHANGES.md as artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: release-changes name: release-changes
path: CHANGES.md path: CHANGES.md
@@ -66,7 +66,7 @@ jobs:
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Download all artifacts - name: Download all artifacts
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with: with:
path: artifacts path: artifacts
- name: Create release bundle with archives for all builds - name: Create release bundle with archives for all builds
@@ -91,7 +91,7 @@ jobs:
artifacts/postgrest-windows-x86-64/postgrest.exe artifacts/postgrest-windows-x86-64/postgrest.exe
- name: Save release bundle - name: Save release bundle
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: release-bundle name: release-bundle
path: release-bundle path: release-bundle
@@ -137,15 +137,15 @@ jobs:
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Download x86-64 Docker image - name: Download x86-64 Docker image
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with: with:
name: postgrest-docker-x86-64 name: postgrest-docker-x86-64
- name: Download aarch64 binary - name: Download aarch64 binary
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with: with:
name: postgrest-ubuntu-aarch64 name: postgrest-ubuntu-aarch64
- uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with: with:
username: ${{ vars.DOCKER_USER }} username: ${{ vars.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }} password: ${{ secrets.DOCKER_PASS }}
@@ -192,7 +192,7 @@ jobs:
github.ref == 'refs/tags/devel' github.ref == 'refs/tags/devel'
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4.0.0 - uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4.0.2
with: with:
username: ${{ vars.DOCKER_USER }} username: ${{ vars.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }} password: ${{ secrets.DOCKER_PASS }}
+1 -1
View File
@@ -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@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1 uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5.4.0
with: with:
files: ./coverage/codecov.json files: ./coverage/codecov.json
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
+32
View File
@@ -5,6 +5,38 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased ## Unreleased
## [12.2.12] - 2025-05-01
### Fixed
- #3956, Fix exposing admin server `/config` by default - @steve-chavez
+ The above endpoint is now disabled unless the `admin-server-config-enabled` config is set to `true`
## [12.2.11] - 2025-04-21
### Fixed
- #4030, Fix regression with parameter `charset=utf-8` in mediatype - @taimoorzaeem
## [12.2.10] - 2025-04-18
### Fixed
- #3889, Fix: JWT cache purging on every request decreases performance - @mkleczek
## [12.2.9] - 2025-04-16
### Fixed
- #3498, Fix incorrect parsing of the `for` parameter of the `application/vnd.pgrst.plan` media type - @taimoorzaeem
- #4014, Fix JWT cache allows old tokens after the jwt-secret is changed in a config reload - @taimoorzaeem
## [12.2.8] - 2025-02-10
### Fixed
- #3841, Log `503` client error to stderr - @taimoorzaeem
## [12.2.7] - 2025-02-03 ## [12.2.7] - 2025-02-03
### Fixed ### Fixed
+1 -1
View File
@@ -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:80dd3c3b9c6cecb9f1667e9290b3bc61b78c2678c02cbdae5f0fea92cc6734ab AS postgrest FROM ubuntu:noble@sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782 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 \
+9 -4
View File
@@ -292,12 +292,17 @@ def setup(app):
app.add_css_file("css/custom.css") app.add_css_file("css/custom.css")
# taken from https://github.com/sphinx-doc/sphinx/blob/82dad44e5bd3776ecb6fd8ded656bc8151d0e63d/sphinx/util/requests.py#L42 user_agent = (
user_agent = "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0"
)
linkcheck_ignore = [ linkcheck_ignore = [
r"https://www.patreon.com/postgrest", # Odd SSL error
r"https://blog.frankel.ch/poor-man-api", r"https://www.dripdepot.com",
# New GitHub UI delays comment load, so anchor fails
r"https://github.com/.*#issuecomment",
# Random 500 Internal Server Error
r"https://jwt.io",
] ]
# sphinx-tabs configuration # sphinx-tabs configuration
+2 -4
View File
@@ -8,9 +8,6 @@ Community Tutorials
* `PostgREST + Auth0: Create REST API in mintutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest/>`_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service. * `PostgREST + Auth0: Create REST API in mintutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest/>`_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service.
* `PostgREST + PostGIS API tutorial in 5 minutes <https://gis-ops.com/postgrest-postgis-api-tutorial-geospatial-api-in-5-minutes/>`_ -
In this tutorial, GIS • OPS shows how to perform PostGIS calculations through PostgREST :ref:`functions` interface.
* `"CodeLess" backend using postgres, postgrest and oauth2 authentication with keycloak <https://www.mathieupassenaud.fr/codeless_backend/>`_ - * `"CodeLess" backend using postgres, postgrest and oauth2 authentication with keycloak <https://www.mathieupassenaud.fr/codeless_backend/>`_ -
A step-by-step tutorial for using PostgREST with KeyCloak(hosted on a managed service). A step-by-step tutorial for using PostgREST with KeyCloak(hosted on a managed service).
@@ -37,6 +34,7 @@ Templates
Example Apps Example Apps
------------ ------------
* `archtika <https://github.com/archtika/archtika>`_ - selfhosted CMS
* `delibrium-postgrest <https://gitlab.com/delibrium/delibrium-postgrest/>`_ - example school API and front-end in Vue.js * `delibrium-postgrest <https://gitlab.com/delibrium/delibrium-postgrest/>`_ - example school API and front-end in Vue.js
* `ETH-transactions-storage <https://github.com/Adamant-im/ETH-transactions-storage>`_ - indexer for Ethereum to get transaction list by ETH address * `ETH-transactions-storage <https://github.com/Adamant-im/ETH-transactions-storage>`_ - indexer for Ethereum to get transaction list by ETH address
* `general <https://github.com/PierreRochard/general>`_ - example auth back-end * `general <https://github.com/PierreRochard/general>`_ - example auth back-end
@@ -90,7 +88,7 @@ Client-Side Libraries
* `postgrest-go <https://github.com/supabase-community/postgrest-go>`_ - Go * `postgrest-go <https://github.com/supabase-community/postgrest-go>`_ - Go
* `postgrest-js <https://github.com/supabase/postgrest-js>`_ - TypeScript/JavaScript * `postgrest-js <https://github.com/supabase/postgrest-js>`_ - TypeScript/JavaScript
* `postgrest-kt <https://github.com/supabase-community/postgrest-kt>`_ - Kotlin * `postgrest-kt <https://github.com/supabase-community/postgrest-kt>`_ - Kotlin
* `postgrest-py <https://github.com/supabase-community/postgrest-py>`_ - Python * `postgrest-py <https://github.com/supabase/postgrest-py>`_ - Python
* `postgrest-rs <https://github.com/supabase-community/postgrest-rs>`_ - Rust * `postgrest-rs <https://github.com/supabase-community/postgrest-rs>`_ - Rust
* `postgrest-swift <https://github.com/supabase-community/postgrest-swift>`_ - Swift * `postgrest-swift <https://github.com/supabase-community/postgrest-swift>`_ - Swift
* `redux-postgrest <https://github.com/andytango/redux-postgrest>`_ - TypeScript/JS, client integrated with (React) Redux. * `redux-postgrest <https://github.com/andytango/redux-postgrest>`_ - TypeScript/JS, client integrated with (React) Redux.
+1 -1
View File
@@ -15,7 +15,7 @@ Roles for Each Web User
PostgREST can accommodate either viewpoint. If you treat a role as a single user then the :ref:`jwt_impersonation` does most of what you need. When an authenticated user makes a request PostgREST will switch into the database role for that user, which in addition to restricting queries, is available to SQL through the :code:`current_user` variable. PostgREST can accommodate either viewpoint. If you treat a role as a single user then the :ref:`jwt_impersonation` does most of what you need. When an authenticated user makes a request PostgREST will switch into the database role for that user, which in addition to restricting queries, is available to SQL through the :code:`current_user` variable.
You can use row-level security to flexibly restrict visibility and access for the current user. Here is an `example <https://www.2ndquadrant.com/en/blog/application-users-vs-row-level-security/>`_ from Tomas Vondra, a chat table storing messages sent between users. Users can insert rows into it to send messages to other users, and query it to see messages sent to them by other users. You can use row-level security to flexibly restrict visibility and access for the current user. Here is an `example <https://www.enterprisedb.com:443/blog/application-users-vs-row-level-security>`_ from Tomas Vondra, a chat table storing messages sent between users. Users can insert rows into it to send messages to other users, and query it to see messages sent to them by other users.
.. code-block:: postgres .. code-block:: postgres
+1 -2
View File
@@ -39,7 +39,7 @@ Sponsors
:target: https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest :target: https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest
.. image:: ../static/gnuhost.png .. image:: ../static/gnuhost.png
:target: https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest :target: https://euronodes.com/?utm_source=sponsor&utm_campaign=postgrest
.. container:: img-dark .. container:: img-dark
@@ -217,7 +217,6 @@ Here are some companies that use PostgREST in production.
* `Nimbus <https://www.nimbusfacility.com/sg/home>`_ * `Nimbus <https://www.nimbusfacility.com/sg/home>`_
- See how Nimbus uses PostgREST in `Paul Copplestone's blog post <https://paul.copplest.one/blog/nimbus-tech-2019-04.html>`_. - See how Nimbus uses PostgREST in `Paul Copplestone's blog post <https://paul.copplest.one/blog/nimbus-tech-2019-04.html>`_.
* `OpenBooking <https://openbooking.ch>`_ * `OpenBooking <https://openbooking.ch>`_
* `Redsmin <https://www.redsmin.com>`_
* `Supabase <https://supabase.com>`_ * `Supabase <https://supabase.com>`_
.. Failing links .. Failing links
+1 -1
View File
@@ -1,6 +1,6 @@
Greenplum Greenplum
######### #########
`Greenplum <https://greenplum.org/>`_ has been reported to work by adding ``LOGIN`` to the :ref:`anonymous and user roles <roles>`. `Greenplum <https://blogs.vmware.com/tanzu/tanzu-greenplum/>`_ has been reported to work by adding ``LOGIN`` to the :ref:`anonymous and user roles <roles>`.
For more details, see https://github.com/PostgREST/postgrest/issues/2021. For more details, see https://github.com/PostgREST/postgrest/issues/2021.
+1 -3
View File
@@ -12,10 +12,10 @@ booleans
BOM BOM
Bytea Bytea
Cardano Cardano
casted
cd cd
centric centric
CLI CLI
CMS
coercible coercible
conf conf
Cloudflare Cloudflare
@@ -146,8 +146,6 @@ SHA
signup signup
SIGUSR SIGUSR
sl sl
spreaded
Spreaded
SQL SQL
sql sql
SQLSTATE SQLSTATE
+5 -1
View File
@@ -55,10 +55,12 @@ Metrics
Provides :ref:`metrics`. Provides :ref:`metrics`.
.. _runtime_config:
Runtime Configuration Runtime Configuration
===================== =====================
Provides a ``config`` endpoint that returns the runtime :ref:`configuration`. Provides a ``config`` endpoint that returns the runtime :ref:`configuration`. This requires setting :ref:`admin-server-config-enabled`.
.. code-block:: bash .. code-block:: bash
@@ -72,6 +74,8 @@ Provides a ``config`` endpoint that returns the runtime :ref:`configuration`.
db-channel-enabled = false db-channel-enabled = false
... ...
.. _runtime_schema_cache:
Runtime Schema Cache Runtime Schema Cache
==================== ====================
+68 -103
View File
@@ -3,20 +3,20 @@
Aggregate Functions Aggregate Functions
################### ###################
Aggregate functions allow you to summarize data by performing calculations across groups of rows. For instance, if you have an ``orders`` table that has an ``amount`` column, you could use an aggregate function to get the sum of the ``amount`` column, either for all rows, or for each group of rows that share specific values, for instance all rows that share the same ``order_date``. PostgREST supports the following aggregate functions: ``avg()``, ``count()``, ``max()``, ``min()``, and ``sum()``.
Please refer to the `section on aggregate functions in the PostgreSQL documentation <https://www.postgresql.org/docs/current/functions-aggregate.html>`_ for a detailed explanation of these functions.
.. note:: .. note::
Aggregate functions are *disabled* by default in PostgREST, as without appropriate safeguards, aggregate functions can create performance problems. See :ref:`db-aggregates-enabled` for further details. Aggregate functions are *disabled* by default in PostgREST, because they can create performance problems without appropriate safeguards.
See :ref:`db-aggregates-enabled` for further details.
PostgREST supports the following aggregate functions: ``avg()``, ``count()``, ``max()``, ``min()``, and ``sum()``. Please refer to the `section on aggregate functions in the PostgreSQL documentation <https://www.postgresql.org/docs/current/functions-aggregate.html>`_ for a detailed explanation of these functions. To use an aggregate function, append it to a column in the ``select`` parameter, like so:
To use an aggregate function, you append the function to a value in the ``select`` parameter, like so:
.. code-block:: bash .. code-block:: bash
curl "http://localhost:3000/orders?select=amount.sum()" curl "http://localhost:3000/orders?select=amount.sum()"
With the above query, PostgREST will return a single row with a single column named ``sum`` that contains the sum of all the values in the ``amount`` column: This will return a ``sum`` of all the values of the ``amount`` column in a single row:
.. code-block:: json .. code-block:: json
@@ -26,15 +26,29 @@ With the above query, PostgREST will return a single row with a single column na
} }
] ]
You can use multiple aggregate functions by just adding more columns with aggregate functions to the ``select`` parameter. You can ``select`` multiple aggregate functions at the same time (you may need to :ref:`rename them <renaming_columns>` to disambiguate).
To group by other columns, you simply add those columns to the ``select`` parameter. For instance: .. code-block:: bash
curl "http://localhost:3000/orders?select=total_amount:amount.sum(),avg_amount:amount.avg(),total_quantity:quantity.sum()"
.. note::
Aggregate functions work alongside other PostgREST features, like :ref:`h_filter`, :ref:`json_columns`, and :ref:`ordering`.
However they are not compatible with :ref:`domain_reps` for the moment.
Additionally, PostgreSQL's ``HAVING`` clause and ordering by aggregated columns are not yet supported.
Automatic ``GROUP BY``
======================
In SQL, a ``GROUP BY`` clause is required to aggregate the selected columns.
However, PostgREST handles grouping automatically if the columns are already present in the ``select`` parameter.
For instance:
.. code-block:: bash .. code-block:: bash
curl "http://localhost:3000/orders?select=amount.sum(),amount.avg(),order_date" curl "http://localhost:3000/orders?select=amount.sum(),amount.avg(),order_date"
This will return a row for each unique value in the ``order_date`` column, with the sum and average of the ``amount`` column for all rows that share the same ``order_date``: This will get the sum and average of the amounts grouped by each unique value in the ``order_date`` column:
.. code-block:: json .. code-block:: json
@@ -51,67 +65,55 @@ This will return a row for each unique value in the ``order_date`` column, with
} }
] ]
.. note:: The ``count()`` Aggregate
Aggregate functions work alongside other PostgREST features, like :ref:`h_filter`, :ref:`json_columns`, and :ref:`ordering`. Please note at this time aggregate functions are not compatible with :ref:`domain_reps`. Additionally, PostgreSQL's ``HAVING`` clause and ordering by aggregated columns are not yet supported. =========================
The Case of ``count()``
===========================
.. note:: .. note::
Before the addition of aggregate functions, it was possible to count by adding ``count`` (without parentheses) to the ``select`` parameter. While this is still supported, it may be deprecated in the future, and thus use of this legacy feature is **not recommended.** Please use ``count()`` (with parentheses) instead. Before the addition of aggregate functions, it was possible to count by adding ``count`` (without parentheses) to the ``select`` parameter.
While this is still supported, it may be deprecated in the future, and thus use of this legacy feature is **not recommended**.
Please use ``count()`` (with parentheses) instead.
``count()`` is a special case because it can be used with or without an aggregated column. For example:
``count()`` is treated specially, as it can be used without an associated column. Take for example the following query:
.. code-block:: bash .. code-block:: bash
curl "http://localhost:3000/orders?select=count(),order_date" curl "http://localhost:3000/orders?select=count(),observation_count:observation.count(),order_date"
This would return a row for each unique value in the ``order_date`` column, with the count of all rows that share the same ``order_date``:
.. code-block:: json .. code-block:: json
[ [
{ {
"count": 4, "count": 4,
"observation_count": 2,
"order_date": "2023-01-01" "order_date": "2023-01-01"
}, },
{ {
"count": 2, "count": 2,
"observation_count": 1,
"order_date": "2023-01-02" "order_date": "2023-01-02"
} }
] ]
When ``count()`` is used with an associated column, its behavior is slightly different: It will return the count of all values that are not ``NULL``. This is due to how PostgreSQL itself implements the ``count()`` function. Note that there is a difference between the result of ``count()`` and ``observation.count()``.
The former counts the whole row, while the latter counts the non ``NULL`` values of the ``observation`` column (both grouped by ``order_date``).
Renaming and Casting This is due to how PostgreSQL itself implements the ``count()`` function.
====================
Renaming Aggregates
-------------------
Just like with other columns, you can rename aggregated columns too. See :ref:`renaming_columns` for details.
Renaming columns is especially helpful in the context of aggregate functions, as by default a column with an aggregate function applied will take on the name of the applied aggregate function. You may want to provide a more semantically meaningful name or prevent collisions when using multiple aggregate functions of the same type.
Casting Aggregates Casting Aggregates
------------------ ==================
When applying an aggregate function to a column, you are able to cast both the value of the input to the aggregate function *and* the value of the output from the aggregate function. In both cases, the syntax works as described in :ref:`casting_columns`, with the only difference being the placement of the cast. It is :ref:`possible to cast <casting_columns>` the aggregated column or the aggregate itself, or both at the same time.
Casting the Value of the Input Casting the Aggregated Column
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -----------------------------
For instance, imagine that the ``orders`` table has a JSON column, ``order_details``, and this column contains a JSON object that has a key, ``tax_amount``. Let's say you want to get the sum of the tax amount for every order. You can use the ``->`` or ``->>`` operators to extract the value with this key (see :ref:`json_columns`), but these operators will return values of the types JSON and ``text`` respectively, and neither of these types can be used with ``sum()``. For example, let's say that ``orders`` has an ``order_details`` :ref:`JSON column <json_columns>` with a ``tax_amount`` key.
We cannot sum ``tax_amount`` directly because using ``->`` or ``->>`` will return the data in ``json`` or ``text`` format.
Therefore, you will need to first cast the input value to a type that is compatible with ``sum()`` (e.g. ``numeric``). Casting the input value is done in exactly the same way as casting any other value: So we need to cast it to a compatible type (e.g. ``numeric``) right before the aggregate function:
.. code-block:: bash .. code-block:: bash
curl "http://localhost:3000/orders?select=order_details->tax_amount::numeric.sum()" curl "http://localhost:3000/orders?select=order_details->tax_amount::numeric.sum()"
With this, you will receive the sum of the casted ``tax_amount`` value:
.. code-block:: json .. code-block:: json
[ [
@@ -120,17 +122,15 @@ With this, you will receive the sum of the casted ``tax_amount`` value:
} }
] ]
Casting the Value of the Output Casting the Aggregate
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ---------------------
Now let's return to an example involving the ``amount`` column of the ``orders`` table. Imagine that we want to get the rounded average of the ``amount`` column. One way to do this is to use the ``avg()`` aggregate function and then to cast the output value of the function to ``int``. To cast the value of the output of the function, we simply place the cast *after* the aggregate function: For instance, if we wanted to round the average of the ``amount`` column, we could do so by casting ``avg()`` to an ``int``:
.. code-block:: bash .. code-block:: bash
curl "http://localhost:3000/orders?select=amount.avg()::int" curl "http://localhost:3000/orders?select=amount.avg()::int"
You will then receive the rounded average as the result:
.. code-block:: json .. code-block:: json
[ [
@@ -139,27 +139,22 @@ You will then receive the rounded average as the result:
} }
] ]
Of course, you can use both input and output casts at the same time, if you so desire. Aggregates and Resource Embedding
=================================
You can group an aggregate function by an :ref:`embedded resource <resource_embedding>` and also use the aggregates inside them.
Using Aggregate Functions with Resource Embedding Grouping by an Embedded Resource
================================================= --------------------------------
Aggregate functions can be used in conjunction with :ref:`resource_embedding`. You can use embedded resources as grouping columns, use aggregate functions within the context of an embedded resource, or use columns from a spreaded resource as grouping columns or as inputs to aggregate functions. Similar to grouping by columns, aggregate functions can also be grouped by embedded resources.
For example, let's say that the ``orders`` table is related to a ``customers`` table.
Using Embedded Resources as Grouping Columns To get the sum of the ``amount`` column grouped by the ``name`` column from the ``customers`` table, we would do the following:
--------------------------------------------
Using an embedded resource as a grouping column allows you to use data from an association to group the results of an aggregation.
For example, imagine that the ``orders`` table from the examples above is related to a ``customers`` table. If you want to get the sum of the ``amount`` column grouped by the ``name`` column from the ``customers`` table, you can include the customer name, using the standard :ref:`resource_embedding` syntax, and perform a sum on the ``amount`` column.
.. code-block:: bash .. code-block:: bash
curl "http://localhost:3000/orders?select=amount.sum(),customers(name)" curl "http://localhost:3000/orders?select=amount.sum(),customers(name)"
You will then get the summed amount, along with the embedded customer resource:
.. code-block:: json .. code-block:: json
[ [
@@ -177,15 +172,16 @@ You will then get the summed amount, along with the embedded customer resource:
} }
] ]
.. note:: The previous example uses a "to-one" relationship, but this can be done on "to-many" relationships as well (although there are few obvious use cases).
The previous example uses a has-one association to demonstrate this functionality, but you may also use has-many associations as grouping columns, although there are few obvious use cases for this.
Using Aggregate Functions Within the Context of an Embedded Resource This also works in a similar way for :ref:`spread embedded resources <spread_embed>`.
-------------------------------------------------------------------- For example, ``select=amount.sum(),...customers(name)`` would sum the ``amount`` grouped by the ``name`` column.
When embedding a resource, you can apply aggregate functions to columns from the associated resource to perform aggregations within the context of an embedded resource. Using Aggregates Inside Embedded Resources
------------------------------------------
Continuing with the example relationship between ``orders`` and ``customers`` from the previous section, imagine that you want to fetch the ``name``, ``city``, and ``state`` for each customer, along with the sum of amount of the customer's orders, grouped by the order date. This can be done in the following way: Using the relationship from the previous example, let's take all the ``customers`` and embed their ``orders``.
If we also want to get the total ``amount`` grouped by the ``order_date`` of the ``orders``, we would do the following:
.. code-block:: bash .. code-block:: bash
@@ -226,51 +222,20 @@ Continuing with the example relationship between ``orders`` and ``customers`` fr
} }
] ]
In this example, the ``amount`` column is summed and grouped by the ``order_date`` *within* the context of the embedded resource. That is, the ``name``, ``city``, and ``state`` from the ``customers`` table have no bearing on the aggregation performed in the context of the ``orders`` association; instead, each aggregation can be seen as being performed independently on just the orders belonging to a particular customer, using only the data from the embedded resource for both grouping and aggregation. Note that the aggregate is done within the embedded resource ``orders``.
It is not affected by any of the columns from the top-level relationship ``customers``.
Using Columns from a Spreaded Resource Using Aggregates in Spreads
-------------------------------------- ~~~~~~~~~~~~~~~~~~~~~~~~~~~
When you :ref:`spread an embedded resource <spread_embed>`, the columns from the spreaded resource are treated as if they were columns of the top-level resource, both when using them as grouping columns and when applying aggregate functions to them. All the aggregates inside a :ref:`spread embedded resource <spread_embed>` will be hoisted to the top-level relationship.
In other words, it will behave as if the aggregate was done in the top-level relationship itself. For example:
Grouping with Columns from a Spreaded Resource
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For instance, assume you want to sum the ``amount`` column from the ``orders`` table, using the ``city`` and ``state`` columns from the ``customers`` table as grouping columns. To achieve this, you may select these two columns from the ``customers`` table and spread them; they will then be used as grouping columns:
.. code-block:: bash .. code-block:: bash
curl "http://localhost:3000/orders?select=amount.sum(),...customers(city,state) curl "http://localhost:3000/orders?select=order_date,...customers(subscription_date.max(),subscription_date.min())
The result will be the same as if ``city`` and ``state`` were columns from the ``orders`` table: This will take the ``max`` and ``min`` subscription date of every customer and group it by the ``order_date`` column:
.. code-block:: json
[
{
"sum": 2000.29,
"city": "New York",
"state": "NY"
},
{
"sum": 9241.21,
"city": "Los Angeles",
"state": "CA"
}
]
Aggregate Functions with Columns from a Spreaded Resource
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Now imagine that the ``customers`` table has a ``joined_date`` column that represents the date that the customer joined. You want to get both the most recent and the oldest ``joined_date`` for customers that placed an order on every distinct order date. This can be expressed as follows:
.. code-block:: bash
curl "http://localhost:3000/orders?select=order_date,...customers(joined_date.max(),joined_date.min())
As columns from a spreaded resource are treated as if they were columns from the top-level resource, the ``max()`` and ``min()`` are applied *within* the context of the top-level, rather than within the context of the embedded resource, as in the previous section.
The result will be the same as if the aggregations were applied to columns from the top-level:
.. code-block:: json .. code-block:: json
+42 -1
View File
@@ -12,7 +12,7 @@ The following preferences are supported.
- ``Prefer: return``. See :ref:`prefer_return`. - ``Prefer: return``. See :ref:`prefer_return`.
- ``Prefer: count``. See :ref:`prefer_count`. - ``Prefer: count``. See :ref:`prefer_count`.
- ``Prefer: resolution``. See :ref:`prefer_resolution`. - ``Prefer: resolution``. See :ref:`prefer_resolution`.
- ``Prefer: missing``. See :ref:`bulk_insert_default`. - ``Prefer: missing``. See :ref:`prefer_missing`.
- ``Prefer: max-affected``, See :ref:`prefer_max_affected`. - ``Prefer: max-affected``, See :ref:`prefer_max_affected`.
- ``Prefer: tx``. See :ref:`prefer_tx`. - ``Prefer: tx``. See :ref:`prefer_tx`.
- ``Prefer: params``. See :ref:`prefer_params`. - ``Prefer: params``. See :ref:`prefer_params`.
@@ -197,6 +197,47 @@ The ``tx`` preference can be set to specify if the :ref:`transaction <transactio
{"id": 35, "name": "Project X"} {"id": 35, "name": "Project X"}
.. _prefer_missing:
Missing
=======
When doing ``POST`` and ``PATCH`` requests, any missing columns in the payload will be inserted as ``null`` value by default. To use the ``DEFAULT`` column value instead, use the ``Prefer: missing=default`` header.
Having:
.. code-block:: postgres
create table foo (
id bigint generated by default as identity primary key
, bar text
, baz int default 100
);
A request:
.. code-block:: bash
curl "http://localhost:3000/foo?columns=id,bar,baz" \
-H "Content-Type: application/json" \
-H "Prefer: missing=default, return=representation" \
-d @- << EOF
[
{ "bar": "val1" },
{ "bar": "val2", "baz": 15 }
]
EOF
Will result in:
.. code-block:: json
[
{ "id": 1, "bar": "val1", "baz": 100 },
{ "id": 2, "bar": "val2", "baz": 15 }
]
.. _prefer_max_affected: .. _prefer_max_affected:
Max Affected Max Affected
@@ -16,6 +16,32 @@ Use the Accept request header to specify the acceptable format (or formats) for
curl "http://localhost:3000/people" \ curl "http://localhost:3000/people" \
-H "Accept: application/json" -H "Accept: application/json"
.. note::
The ordering of columns in the response isn't guaranteed to align with the order specified in the ``select`` clause. For example, with resource embedding:
.. code-block:: bash
http://localhost:3000/films?select=directors(last_name,id),title
We may get:
.. code-block:: bash
[
{
"title": "title",
"directors": {
"id": 5,
"last_name": "name"
}
}
]
This is in line with the `JSON schema spec <https://json-schema.org/draft/2020-12/json-schema-core#name-instance-data-model>`_:
*"object: An unordered set of properties mapping a string to an instance"*
.. _builtin_media: .. _builtin_media:
Builtin Media Type Handlers Builtin Media Type Handlers
+9 -41
View File
@@ -525,45 +525,6 @@ To bulk insert JSON post an array of objects having all-matching keys
] ]
EOF EOF
.. _bulk_insert_default:
Bulk Insert with Default Values
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Any missing columns in the payload will be inserted as ``null`` values. To use the ``DEFAULT`` column value instead, use the ``Prefer: missing=default`` header.
Having:
.. code-block:: postgres
create table foo (
id bigint generated by default as identity primary key
, bar text
, baz int default 100
);
A request:
.. code-block:: bash
curl "http://localhost:3000/foo?columns=id,bar,baz" \
-H "Content-Type: application/json" \
-H "Prefer: missing=default, return=representation" \
-d @- << EOF
[
{ "bar": "val1" },
{ "bar": "val2", "baz": 15 }
]
EOF
Will result in:
.. code-block:: json
[
{ "id": 1, "bar": "val1", "baz": 100 },
{ "id": 2, "bar": "val2", "baz": 15 }
]
.. _specify_columns: .. _specify_columns:
@@ -606,7 +567,13 @@ To update a row or rows in a table, use the PATCH verb. Use :ref:`h_filter` to s
-X PATCH -H "Content-Type: application/json" \ -X PATCH -H "Content-Type: application/json" \
-d '{ "category": "child" }' -d '{ "category": "child" }'
Updates also support :ref:`prefer_return`, :ref:`resource_embedding` and :ref:`v_filter`. Updates also support:
- :ref:`prefer_return`
- :ref:`resource_embedding`
- :ref:`v_filter`
- :ref:`Missing Preference <prefer_missing>`
- :ref:`specify_columns`
.. warning:: .. warning::
@@ -637,7 +604,7 @@ You can make an upsert with :code:`POST` and the :code:`Prefer: resolution=merge
By default, upsert operates based on the primary key columns, so you must specify all of them. By default, upsert operates based on the primary key columns, so you must specify all of them.
You can also choose to ignore the duplicates with :code:`Prefer: resolution=ignore-duplicates`. You can also choose to ignore the duplicates with :code:`Prefer: resolution=ignore-duplicates`.
Upsert works best when the primary key is natural (e.g. ``sku``). Upsert works best when the primary key is natural (e.g. ``sku``).
However, it can work with surrogate primary keys (e.g. ``id serial primary key``), if you also do a :ref:`bulk_insert_default`: However, it can work with surrogate primary keys (e.g. ``id serial primary key``), if you also do a :ref:`bulk_insert` with :ref:`prefer_missing`:
.. code-block:: bash .. code-block:: bash
@@ -755,6 +722,7 @@ Using ``offset`` to target a different subset of rows is also possible.
'#estimated-count': 'pagination_count.html#estimated-count', '#estimated-count': 'pagination_count.html#estimated-count',
'#prefer-return-headers-only': 'preferences.html#headers-only', '#prefer-return-headers-only': 'preferences.html#headers-only',
'#prefer-return-representation': 'preferences.html#full', '#prefer-return-representation': 'preferences.html#full',
'#bulk-insert-default': 'preferences.html#prefer-missing',
}; };
let willRedirectTo = redirects[hash]; let willRedirectTo = redirects[hash];
+9 -1
View File
@@ -159,7 +159,15 @@ You can specify the literal value as we saw earlier, or reference a filename to
JWT Claims Validation JWT Claims Validation
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
PostgREST honors the :code:`exp` claim for token expiration, rejecting expired tokens. PostgREST honors the following `JWT claims <https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4>`_:
- ``exp`` Expiration Time
- ``iat`` Issued At
- ``nbf`` Not Before
- ``aud`` Audience, see :ref:`jwt-aud`
.. note::
PostgREST allows for a 30-second clock skew when validating the ``exp`` and ``iat`` claims. In other words, it gives an extra 30 seconds before the token is rejected if there is a slight discrepancy in the timestamps.
JWT Security JWT Security
~~~~~~~~~~~~ ~~~~~~~~~~~~
+24
View File
@@ -161,6 +161,30 @@ admin-server-port
Specifies the port for the :ref:`admin_server`. Specifies the port for the :ref:`admin_server`.
.. _admin-server-config-enabled:
admin-server-config-enabled
---------------------------
.. danger::
The ``/config`` endpoint contains sensitive information, don't enable this if you're exposing the Admin Server publicly.
To safely enable this you can use a proxy like :ref:`nginx` to:
- Ensure ``/config`` are only available to local networks.
- Only expose ``/live`` and ``/ready`` to public networks.
=============== =================================
**Type** Boolean
**Default** False
**Reloadable** N
**Environment** PGRST_ADMIN_SERVER_CONFIG_ENABLED
**In-Database** `n/a`
=============== =================================
Enables the admin server :ref:`runtime_config` and :ref:`runtime_schema_cache` endpoints.
.. _app.settings.*: .. _app.settings.*:
app.settings.* app.settings.*
+1 -1
View File
@@ -34,7 +34,7 @@
.. code:: bash .. code:: bash
nix-env -i haskellPackages.postgrest nix-env -i postgrest
.. group-tab:: Windows .. group-tab:: Windows
+1 -1
View File
@@ -1,5 +1,5 @@
name: postgrest name: postgrest
version: 12.2.7 version: 12.2.12
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
+4 -1
View File
@@ -56,8 +56,11 @@ admin appState req respond = do
in in
respond $ Wai.responseLBS status [] mempty respond $ Wai.responseLBS status [] mempty
["config"] -> do ["config"] -> do
config <- AppState.getConfig appState config@Config.AppConfig{configAdminServerConfigEnabled} <- AppState.getConfig appState
if configAdminServerConfigEnabled then
respond $ Wai.responseLBS HTTP.status200 [] (LBS.fromStrict $ encodeUtf8 $ Config.toText config) respond $ Wai.responseLBS HTTP.status200 [] (LBS.fromStrict $ encodeUtf8 $ Config.toText config)
else
respond $ Wai.responseLBS HTTP.status404 [] mempty
["schema_cache"] -> do ["schema_cache"] -> do
sCache <- AppState.getSchemaCache appState sCache <- AppState.getSchemaCache appState
respond $ Wai.responseLBS HTTP.status200 [] (maybe mempty JSON.encode sCache) respond $ Wai.responseLBS HTTP.status200 [] (maybe mempty JSON.encode sCache)
+40 -14
View File
@@ -5,6 +5,7 @@
module PostgREST.AppState module PostgREST.AppState
( AppState ( AppState
, AuthResult(..) , AuthResult(..)
, JwtCacheState(..)
, destroy , destroy
, getConfig , getConfig
, getSchemaCache , getSchemaCache
@@ -13,7 +14,7 @@ module PostgREST.AppState
, getNextDelay , getNextDelay
, getNextListenerDelay , getNextListenerDelay
, getTime , getTime
, getJwtCache , getJwtCacheState
, getSocketREST , getSocketREST
, getSocketAdmin , getSocketAdmin
, init , init
@@ -83,6 +84,12 @@ data AuthResult = AuthResult
, authRole :: BS.ByteString , authRole :: BS.ByteString
} }
-- | JWT Cache and IO action that triggers purging old entries from the cache
data JwtCacheState = JwtCacheState
{ jwtCache :: C.Cache ByteString AuthResult
, purgeCache :: IO ()
}
data AppState = AppState data AppState = AppState
-- | Database connection pool -- | Database connection pool
{ statePool :: SQL.Pool { statePool :: SQL.Pool
@@ -107,7 +114,7 @@ data AppState = AppState
-- | Keeps track of the next delay for the listener -- | Keeps track of the next delay for the listener
, stateNextListenerDelay :: IORef Int , stateNextListenerDelay :: IORef Int
-- | JWT Cache -- | JWT Cache
, jwtCache :: C.Cache ByteString AuthResult , jwtCacheState :: JwtCacheState
-- | Network socket for REST API -- | Network socket for REST API
, stateSocketREST :: NS.Socket , stateSocketREST :: NS.Socket
-- | Network socket for the admin UI -- | Network socket for the admin UI
@@ -139,6 +146,16 @@ init conf@AppConfig{configLogLevel, configDbPoolSize} = do
initWithPool :: AppSockets -> SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState initWithPool :: AppSockets -> SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState
initWithPool (sock, adminSock) pool conf loggerState metricsState observer = do initWithPool (sock, adminSock) pool conf loggerState metricsState observer = do
cache <- C.newCache Nothing
-- purgeExpired has O(n^2) complexity
-- so we wrap it in debounce to make sure it:
-- 1) is executed asynchronously
-- 2) only a single purge operation is running at a time
debounce <- mkDebounce defaultDebounceSettings
-- debounceFreq is set to default 1 second
{ debounceAction = C.purgeExpired cache
, debounceEdge = leadingEdge
}
appState <- AppState pool appState <- AppState pool
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step <$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
@@ -151,7 +168,7 @@ initWithPool (sock, adminSock) pool conf loggerState metricsState observer = do
<*> myThreadId <*> myThreadId
<*> newIORef 0 <*> newIORef 0
<*> newIORef 1 <*> newIORef 1
<*> C.newCache Nothing <*> pure (JwtCacheState cache debounce)
<*> pure sock <*> pure sock
<*> pure adminSock <*> pure adminSock
<*> pure observer <*> pure observer
@@ -265,8 +282,9 @@ usePool AppState{stateObserver=observer, stateMainThreadId=mainThreadId, ..} ses
SQL.ServerError{} -> SQL.ServerError{} ->
when (Error.status (Error.PgError False err) >= HTTP.status500) $ when (Error.status (Error.PgError False err) >= HTTP.status500) $
observer $ QueryErrorCodeHighObs err observer $ QueryErrorCodeHighObs err
SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _)) -> err@(SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _))) ->
pure () -- An error on the client-side, usually indicates problems wth connection
observer $ QueryErrorCodeHighObs err
) )
return res return res
@@ -313,8 +331,8 @@ putConfig = atomicWriteIORef . stateConf
getTime :: AppState -> IO UTCTime getTime :: AppState -> IO UTCTime
getTime = stateGetTime getTime = stateGetTime
getJwtCache :: AppState -> C.Cache ByteString AuthResult getJwtCacheState :: AppState -> JwtCacheState
getJwtCache = jwtCache getJwtCacheState = jwtCacheState
getSocketREST :: AppState -> NS.Socket getSocketREST :: AppState -> NS.Socket
getSocketREST = stateSocketREST getSocketREST = stateSocketREST
@@ -438,12 +456,12 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
-- | Reads the in-db config and reads the config file again -- | Reads the in-db config and reads the config file again
-- | We don't retry reading the in-db config after it fails immediately, because it could have user errors. We just report the error and continue. -- | We don't retry reading the in-db config after it fails immediately, because it could have user errors. We just report the error and continue.
readInDbConfig :: Bool -> AppState -> IO () readInDbConfig :: Bool -> AppState -> IO ()
readInDbConfig startingUp appState@AppState{stateObserver=observer} = do readInDbConfig startingUp appState@AppState{stateObserver=observer, jwtCacheState=JwtCacheState{jwtCache}} = do
AppConfig{..} <- getConfig appState conf <- getConfig appState
pgVer <- getPgVersion appState pgVer <- getPgVersion appState
dbSettings <- dbSettings <-
if configDbConfig then do if configDbConfig conf then do
qDbSettings <- usePool appState (queryDbSettings (dumpQi <$> configDbPreConfig) configDbPreparedStatements) qDbSettings <- usePool appState (queryDbSettings (dumpQi <$> configDbPreConfig conf) (configDbPreparedStatements conf))
case qDbSettings of case qDbSettings of
Left e -> do Left e -> do
observer $ ConfigReadErrorObs e observer $ ConfigReadErrorObs e
@@ -452,8 +470,8 @@ readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
else else
pure mempty pure mempty
(roleSettings, roleIsolationLvl) <- (roleSettings, roleIsolationLvl) <-
if configDbConfig then do if configDbConfig conf then do
rSettings <- usePool appState (queryRoleSettings pgVer configDbPreparedStatements) rSettings <- usePool appState (queryRoleSettings pgVer (configDbPreparedStatements conf))
case rSettings of case rSettings of
Left e -> do Left e -> do
observer $ QueryRoleSettingsErrorObs e observer $ QueryRoleSettingsErrorObs e
@@ -461,7 +479,7 @@ readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
Right x -> pure x Right x -> pure x
else else
pure mempty pure mempty
readAppConfig dbSettings configFilePath (Just configDbUri) roleSettings roleIsolationLvl >>= \case readAppConfig dbSettings (configFilePath conf) (Just $ configDbUri conf) roleSettings roleIsolationLvl >>= \case
Left err -> Left err ->
if startingUp then if startingUp then
panic err -- die on invalid config if the program is starting up panic err -- die on invalid config if the program is starting up
@@ -469,6 +487,14 @@ readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
observer $ ConfigInvalidObs err observer $ ConfigInvalidObs err
Right newConf -> do Right newConf -> do
putConfig appState newConf putConfig appState newConf
-- After the config has reloaded, jwt-secret might have changed, so
-- if it has changed, it is important to invalidate the jwt cache
-- entries, because they were cached using the old secret
if configJwtSecret conf == configJwtSecret newConf then
pass
else
C.purge jwtCache -- atomic O(1) operation
if startingUp then if startingUp then
pass pass
else else
+10 -8
View File
@@ -44,8 +44,9 @@ import System.Clock (TimeSpec (..))
import System.IO.Unsafe (unsafePerformIO) import System.IO.Unsafe (unsafePerformIO)
import System.TimeIt (timeItT) import System.TimeIt (timeItT)
import PostgREST.AppState (AppState, AuthResult (..), getConfig, import PostgREST.AppState (AppState, AuthResult (..),
getJwtCache, getTime) JwtCacheState (..), getConfig,
getJwtCacheState, getTime)
import PostgREST.Config (AppConfig (..), JSPath, JSPathExp (..)) import PostgREST.Config (AppConfig (..), JSPath, JSPathExp (..))
import PostgREST.Error (Error (..)) import PostgREST.Error (Error (..))
@@ -131,7 +132,8 @@ middleware appState app req respond = do
-- | Used to retrieve and insert JWT to JWT Cache -- | Used to retrieve and insert JWT to JWT Cache
getJWTFromCache :: AppState -> ByteString -> Int -> IO (Either Error AuthResult) -> UTCTime -> IO (Either Error AuthResult) getJWTFromCache :: AppState -> ByteString -> Int -> IO (Either Error AuthResult) -> UTCTime -> IO (Either Error AuthResult)
getJWTFromCache appState token maxLifetime parseJwt utc = do getJWTFromCache appState token maxLifetime parseJwt utc = do
checkCache <- C.lookup (getJwtCache appState) token let JwtCacheState{..} = getJwtCacheState appState
checkCache <- C.lookup jwtCache token
authResult <- maybe parseJwt (pure . Right) checkCache authResult <- maybe parseJwt (pure . Right) checkCache
case (authResult,checkCache) of case (authResult,checkCache) of
@@ -151,17 +153,17 @@ getJWTFromCache appState token maxLifetime parseJwt utc = do
let timeSpec = getTimeSpec res maxLifetime utc let timeSpec = getTimeSpec res maxLifetime utc
-- purge expired cache entries
C.purgeExpired jwtCache
-- insert new cache entry -- insert new cache entry
C.insert' jwtCache timeSpec token res C.insert' jwtCache timeSpec token res
-- Execute IO action to purge the cache
-- It is assumed this action returns immidiately
-- so that request processing is not blocked.
purgeCache
_ -> pure () _ -> pure ()
return authResult return authResult
where
jwtCache = getJwtCache appState
-- Used to extract JWT exp claim and add to JWT Cache -- Used to extract JWT exp claim and add to JWT Cache
getTimeSpec :: AuthResult -> Int -> UTCTime -> Maybe TimeSpec getTimeSpec :: AuthResult -> Int -> UTCTime -> Maybe TimeSpec
+3
View File
@@ -128,6 +128,9 @@ exampleConfigFile =
[str|## Admin server used for checks. It's disabled by default unless a port is specified. [str|## Admin server used for checks. It's disabled by default unless a port is specified.
|# admin-server-port = 3001 |# admin-server-port = 3001
| |
|## Whether to enable the /config endpoint of the admin server
|# admin-server-config-enabled = false
|
|## The database role to use when no client authentication is provided |## The database role to use when no client authentication is provided
|# db-anon-role = "anon" |# db-anon-role = "anon"
| |
+3
View File
@@ -110,6 +110,7 @@ data AppConfig = AppConfig
, configServerUnixSocket :: Maybe FilePath , configServerUnixSocket :: Maybe FilePath
, configServerUnixSocketMode :: FileMode , configServerUnixSocketMode :: FileMode
, configAdminServerPort :: Maybe Int , configAdminServerPort :: Maybe Int
, configAdminServerConfigEnabled :: Bool
, configRoleSettings :: RoleSettings , configRoleSettings :: RoleSettings
, configRoleIsoLvl :: RoleIsolationLvl , configRoleIsoLvl :: RoleIsolationLvl
, configInternalSCSleep :: Maybe Int32 , configInternalSCSleep :: Maybe Int32
@@ -180,6 +181,7 @@ toText conf =
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket) ,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
,("server-unix-socket-mode", q . T.pack . showSocketMode) ,("server-unix-socket-mode", q . T.pack . showSocketMode)
,("admin-server-port", maybe "\"\"" show . configAdminServerPort) ,("admin-server-port", maybe "\"\"" show . configAdminServerPort)
,("admin-server-config-enabled", T.toLower . show . configAdminServerConfigEnabled)
] ]
-- quote all app.settings -- quote all app.settings
@@ -286,6 +288,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
<*> (fmap T.unpack <$> optString "server-unix-socket") <*> (fmap T.unpack <$> optString "server-unix-socket")
<*> parseSocketFileMode "server-unix-socket-mode" <*> parseSocketFileMode "server-unix-socket-mode"
<*> optInt "admin-server-port" <*> optInt "admin-server-port"
<*> (fromMaybe False <$> optBool "admin-server-config-enabled")
<*> pure roleSettings <*> pure roleSettings
<*> pure roleIsolationLvl <*> pure roleIsolationLvl
<*> optInt "internal-schema-cache-sleep" <*> optInt "internal-schema-cache-sleep"
+47 -24
View File
@@ -1,7 +1,7 @@
{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# OPTIONS_GHC -Wno-unused-do-bind #-}
module PostgREST.MediaType module PostgREST.MediaType
( MediaType(..) ( MediaType(..)
, MTVndPlanOption (..) , MTVndPlanOption (..)
@@ -13,14 +13,13 @@ module PostgREST.MediaType
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.Text as T
import Network.HTTP.Types.Header (Header, hContentType) import qualified Text.ParserCombinators.Parsec as P
import Data.Map (fromList, (!?)) import Data.Map (fromList, (!?))
import qualified Data.Text as T (break, drop, dropWhile,
dropWhileEnd, null, splitOn,
toLower)
import Data.Text.Encoding (decodeLatin1) import Data.Text.Encoding (decodeLatin1)
import Network.HTTP.Types.Header (Header, hContentType)
import Protolude import Protolude
-- | Enumeration of currently supported media types -- | Enumeration of currently supported media types
@@ -104,6 +103,9 @@ toMimePlanFormat PlanText = "text"
-- >>> decodeMediaType "application/vnd.pgrst.plan;for=\"application/json\"" -- >>> decodeMediaType "application/vnd.pgrst.plan;for=\"application/json\""
-- MTVndPlan MTApplicationJSON PlanText [] -- MTVndPlan MTApplicationJSON PlanText []
-- --
-- >>> decodeMediaType "application/vnd.pgrst.plan ; for=\"text/xml\" ; options=analyze"
-- MTVndPlan MTTextXML PlanText [PlanAnalyze]
--
-- >>> decodeMediaType "application/vnd.pgrst.plan+json;for=\"text/csv\"" -- >>> decodeMediaType "application/vnd.pgrst.plan+json;for=\"text/csv\""
-- MTVndPlan MTTextCSV PlanJSON [] -- MTVndPlan MTTextCSV PlanJSON []
-- --
@@ -150,7 +152,10 @@ decodeMediaType mt = decodeMediaType' $ decodeLatin1 mt
("*","*",_) -> MTAny ("*","*",_) -> MTAny
_ -> MTOther mt' _ -> MTOther mt'
where where
(mainType, subType, params') = tokenizeMediaType mt' mediaTypeOrError = P.parse tokenizeMediaType "parsec: tokenizeMediaType failed" $ T.unpack mt'
(mainType, subType, params') = case mediaTypeOrError of
Right mt'' -> mt''
Left _ -> (mt',"",[])
params = fromList $ map (first T.toLower) params' -- normalize parameter names to lowercase, per RFC 7321 params = fromList $ map (first T.toLower) params' -- normalize parameter names to lowercase, per RFC 7321
getPlan fmt = MTVndPlan mtFor fmt $ getPlan fmt = MTVndPlan mtFor fmt $
[PlanAnalyze | inOpts "analyze" ] ++ [PlanAnalyze | inOpts "analyze" ] ++
@@ -166,21 +171,39 @@ decodeMediaType mt = decodeMediaType' $ decodeLatin1 mt
checkArrayNullStrip = if strippedNulls then MTVndArrayJSONStrip else MTApplicationJSON checkArrayNullStrip = if strippedNulls then MTVndArrayJSONStrip else MTApplicationJSON
-- | Split a Media Type string into components -- | Split a Media Type string into components
-- >>> tokenizeMediaType "application/vnd.pgrst.plan+json;for=\"text/csv\"" -- >>> P.parse tokenizeMediaType "" "application/vnd.pgrst.plan+json;for=\"text/csv\""
-- ("application","vnd.pgrst.plan+json",[("for","text/csv")]) -- Right ("application","vnd.pgrst.plan+json",[("for","text/csv")])
-- >>> tokenizeMediaType "*/*" --
-- ("*","*",[]) -- >>> P.parse tokenizeMediaType "" "*/*"
-- >>> tokenizeMediaType "application/vnd.pgrst.plan;wat=\"application/json;text/csv\"" -- Right ("*","*",[])
-- ("application","vnd.pgrst.plan",[("wat","application/json"),("text/csv\"","")]) --
tokenizeMediaType :: Text -> (Text, Text, [(Text, Text)]) -- >>> P.parse tokenizeMediaType "" "application/vnd.pgrst.plan;wat=\"application/json;text/csv\""
tokenizeMediaType t = (mainType, subType, params) -- Right ("application","vnd.pgrst.plan",[("wat","application/json;text/csv")])
--
-- >>> P.parse tokenizeMediaType "" "application/vnd.pgrst.plan+text; for=\"text/xml\"; options=analyze|verbose|settings|buffers|wal"
-- Right ("application","vnd.pgrst.plan+text",[("for","text/xml"),("options","analyze|verbose|settings|buffers|wal")])
-- TODO: Improve mediatype parser as per RFC 2045 https://datatracker.ietf.org/doc/html/rfc2045#section-5.1
tokenizeMediaType :: P.Parser (Text, Text, [(Text, Text)])
tokenizeMediaType = do
mainType <- P.many1 (P.alphaNum <|> P.oneOf ".*")
P.char '/'
subType <- P.many1 (P.alphaNum <|> P.oneOf ".*+-")
params <- P.many pSemicolonSeparatedKeyVals
P.optional $ P.try $ P.spaces *> P.char ';' -- ending semicolon, discard input after that because it has already failed or we have hit EOF
return (T.pack mainType, T.pack subType, params)
where where
(mainType, rest) = T.break (== '/') t pSemicolonSeparatedKeyVals :: P.Parser (Text, Text)
(subType, restParams) = T.break (== ';') $ T.drop 1 rest pSemicolonSeparatedKeyVals = P.try $ P.spaces *> P.char ';' *> P.spaces *> pKeyVal
params = where
let rp = T.drop 1 restParams pKeyVal :: P.Parser (Text, Text)
in if T.null rp then [] else map param $ T.splitOn ";" rp -- FIXME: breaks if there's a ';' in a quoted value pKeyVal = do
param p = key <- P.many1 (P.alphaNum <|> P.oneOf "-")
let (k, v) = T.break (== '=') p P.spaces
in (k, dropAround (== '"') $ T.drop 1 v) -- FIXME: doesn't unescape quotes in values P.char '='
dropAround p = T.dropWhile p . T.dropWhileEnd p P.spaces
val <- P.try pQuoted <|> P.try pUnQuoted
return (T.pack key, T.pack val)
where
pUnQuoted = P.many1 (P.alphaNum <|> P.oneOf "|-")
pQuoted = P.char '\"' *> P.manyTill P.anyChar (P.char '\"')
+1
View File
@@ -36,3 +36,4 @@ server-timing-enabled = false
server-unix-socket = "" server-unix-socket = ""
server-unix-socket-mode = "660" server-unix-socket-mode = "660"
admin-server-port = "" admin-server-port = ""
admin-server-config-enabled = false
@@ -36,3 +36,4 @@ server-timing-enabled = false
server-unix-socket = "" server-unix-socket = ""
server-unix-socket-mode = "660" server-unix-socket-mode = "660"
admin-server-port = "" admin-server-port = ""
admin-server-config-enabled = false
@@ -36,3 +36,4 @@ server-timing-enabled = false
server-unix-socket = "" server-unix-socket = ""
server-unix-socket-mode = "660" server-unix-socket-mode = "660"
admin-server-port = "" admin-server-port = ""
admin-server-config-enabled = false
+1
View File
@@ -36,3 +36,4 @@ server-timing-enabled = false
server-unix-socket = "" server-unix-socket = ""
server-unix-socket-mode = "660" server-unix-socket-mode = "660"
admin-server-port = "" admin-server-port = ""
admin-server-config-enabled = false
@@ -36,5 +36,6 @@ server-timing-enabled = true
server-unix-socket = "/tmp/pgrst_io_test.sock" server-unix-socket = "/tmp/pgrst_io_test.sock"
server-unix-socket-mode = "777" server-unix-socket-mode = "777"
admin-server-port = 3001 admin-server-port = 3001
admin-server-config-enabled = true
app.settings.test = "test" app.settings.test = "test"
app.settings.test2 = "test" app.settings.test2 = "test"
@@ -36,5 +36,6 @@ server-timing-enabled = false
server-unix-socket = "/tmp/pgrst_io_test.sock" server-unix-socket = "/tmp/pgrst_io_test.sock"
server-unix-socket-mode = "777" server-unix-socket-mode = "777"
admin-server-port = 3001 admin-server-port = 3001
admin-server-config-enabled = true
app.settings.test = "test" app.settings.test = "test"
app.settings.test2 = "test" app.settings.test2 = "test"
@@ -36,5 +36,6 @@ server-timing-enabled = true
server-unix-socket = "/tmp/pgrst_io_test.sock" server-unix-socket = "/tmp/pgrst_io_test.sock"
server-unix-socket-mode = "777" server-unix-socket-mode = "777"
admin-server-port = 3001 admin-server-port = 3001
admin-server-config-enabled = true
app.settings.test = "test" app.settings.test = "test"
app.settings.test2 = "test" app.settings.test2 = "test"
+1
View File
@@ -36,4 +36,5 @@ server-timing-enabled = false
server-unix-socket = "" server-unix-socket = ""
server-unix-socket-mode = "660" server-unix-socket-mode = "660"
admin-server-port = "" admin-server-port = ""
admin-server-config-enabled = false
app.settings.test = "Bool False" app.settings.test = "Bool False"
+1
View File
@@ -39,3 +39,4 @@ PGRST_SERVER_TIMING_ENABLED: true
PGRST_SERVER_UNIX_SOCKET: /tmp/pgrst_io_test.sock PGRST_SERVER_UNIX_SOCKET: /tmp/pgrst_io_test.sock
PGRST_SERVER_UNIX_SOCKET_MODE: 777 PGRST_SERVER_UNIX_SOCKET_MODE: 777
PGRST_ADMIN_SERVER_PORT: 3001 PGRST_ADMIN_SERVER_PORT: 3001
PGRST_ADMIN_SERVER_CONFIG_ENABLED: true
+1
View File
@@ -36,5 +36,6 @@ server-timing-enabled = true
server-unix-socket = "/tmp/pgrst_io_test.sock" server-unix-socket = "/tmp/pgrst_io_test.sock"
server-unix-socket-mode = "777" server-unix-socket-mode = "777"
admin-server-port = 3001 admin-server-port = 3001
admin-server-config-enabled = true
app.settings.test = "test" app.settings.test = "test"
app.settings.test2 = "test" app.settings.test2 = "test"
+63
View File
@@ -673,6 +673,15 @@ def test_admin_config(defaultenv):
"Should get a success response from the admin server containing current configuration" "Should get a success response from the admin server containing current configuration"
with run(env=defaultenv) as postgrest: with run(env=defaultenv) as postgrest:
response = postgrest.admin.get("/config")
assert response.status_code == 404
env = {
**defaultenv,
"PGRST_ADMIN_SERVER_CONFIG_ENABLED": "true",
}
with run(env=env) as postgrest:
response = postgrest.admin.get("/config") response = postgrest.admin.get("/config")
print(response.text) print(response.text)
assert response.status_code == 200 assert response.status_code == 200
@@ -1644,3 +1653,57 @@ def test_jwt_cache_purges_expired_entries(defaultenv):
response = postgrest.session.get("/authors_only", headers=hdrs3) response = postgrest.session.get("/authors_only", headers=hdrs3)
assert response.status_code == 200 assert response.status_code == 200
def test_pgrst_log_503_client_error_to_stderr(defaultenv):
"PostgREST should log 503 errors to stderr"
env = {
**defaultenv,
"PGAPPNAME": "test-io",
}
with run(env=env) as postgrest:
postgrest.session.get("/rpc/terminate_pgrst?appname=test-io")
output = postgrest.read_stdout(nlines=6)
log_message = '{"code":"PGRST001","details":"no connection to the server\\n","hint":null,"message":"Database client error. Retrying the connection."}\n'
assert any(log_message in line for line in output)
def test_invalidate_jwt_cache_when_secret_changes(tmp_path, defaultenv):
"JWT cache should be emptied after jwt-secret is changed in a config reload"
headers = jwtauthheader({"role": "postgrest_test_author"}, SECRET)
external_secret_file = tmp_path / "jwt-secret-config"
external_secret_file.write_text(SECRET)
env = {
**defaultenv,
"PGRST_JWT_SECRET": f"@{external_secret_file}",
"PGRST_DB_CHANNEL_ENABLED": "true",
"PGRST_JWT_CACHE_MAX_LIFETIME": "86400", # enable cache
"PGRST_DB_ANON_ROLE": "postgrest_test_anonymous", # required for NOTIFY
}
with run(env=env) as postgrest:
response = postgrest.session.get("/authors_only", headers=headers)
assert response.status_code == 200 # jwt gets cached
# change external file
external_secret_file.write_text("invalid" * 5)
# reload config and external file with NOTIFY
# jwt-cache should get empty
response = postgrest.session.post("/rpc/reload_pgrst_config")
assert response.text == ""
assert response.status_code == 204
sleep_until_postgrest_config_reload()
# now the request should fail because the cached token is removed
response = postgrest.session.get("/authors_only", headers=headers)
assert response.status_code == 401
+2 -2
View File
@@ -103,8 +103,8 @@ postJsonArrayTest(){
echo "Running memory usage tests.." echo "Running memory usage tests.."
jsonKeyTest "1M" "POST" "/rpc/leak?columns=blob" "27M" jsonKeyTest "1M" "POST" "/rpc/leak?columns=blob" "27M"
jsonKeyTest "1M" "POST" "/leak?columns=blob" "20M" jsonKeyTest "1M" "POST" "/leak?columns=blob" "21M"
jsonKeyTest "1M" "PATCH" "/leak?id=eq.1&columns=blob" "20M" jsonKeyTest "1M" "PATCH" "/leak?id=eq.1&columns=blob" "21M"
jsonKeyTest "10M" "POST" "/rpc/leak?columns=blob" "32M" jsonKeyTest "10M" "POST" "/rpc/leak?columns=blob" "32M"
jsonKeyTest "10M" "POST" "/leak?columns=blob" "32M" jsonKeyTest "10M" "POST" "/leak?columns=blob" "32M"
@@ -375,3 +375,31 @@ spec = describe "custom media types" $ do
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/octet-stream"] , matchHeaders = ["Content-Type" <:> "application/octet-stream"]
} }
context "media type parser fails" $ do
it "sends media type as is" $
request methodGet "/items" (acceptHdrs "undefined") ""
`shouldRespondWith`
[json| {"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: undefined"} |]
{ matchStatus = 406 }
context "media type parser allowed characters" $ do
it "regression test allowing charset=utf-8" $
request methodPost "/rpc/overloaded_default"
[("Content-Type", "application/json; charset=utf-8")]
[json|{"must_param":1}|]
`shouldRespondWith`
[json|{"val":1}|]
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
}
it "handle unrecognized parameters leniently" $ do
request methodPost "/rpc/overloaded_default"
[("Content-Type", "application/json; $$ unrecognized-chars=ignored $$")]
[json|{"must_param":1}|]
`shouldRespondWith`
[json|{"val":1}|]
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
}
+1
View File
@@ -151,6 +151,7 @@ baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configDbTxAllowOverride = True , configDbTxAllowOverride = True
, configDbTxRollbackAll = True , configDbTxRollbackAll = True
, configAdminServerPort = Nothing , configAdminServerPort = Nothing
, configAdminServerConfigEnabled = False
, configRoleSettings = mempty , configRoleSettings = mempty
, configRoleIsoLvl = mempty , configRoleIsoLvl = mempty
, configInternalSCSleep = Nothing , configInternalSCSleep = Nothing