Compare commits

..
Author SHA1 Message Date
Laurence Isla 97b6022f5a bump version to 13.0.2 2025-06-02 14:18:26 -05:00
Laurence Isla c413833ec8 fix: regression that makes order by with nulls order not work alongside limits 2025-06-02 14:17:35 -05:00
steve-chavez dee7d6f39c bump version to 13.0.1 2025-06-01 08:10:43 -05:00
Thilo HohltandWolfgang Walther 6fb8077950 Update ecosystem.rst
The repository has been moved from a dedicated organisation to my personal profile, so this old link will no longer work after 90 days.
2025-05-31 13:50:41 +02:00
steve-chavez d4f82919f7 docs: external authentication page in explanations
- Move page from External JWT generation
2025-05-30 18:08:40 -05:00
steve-chavez 610a8be9c4 docs: move jwt using SSL to tutorial 1
Removes jwt.io example in favor of a bash script
2025-05-30 18:08:34 -05:00
Taimoor ZaeemandWolfgang Walther ba42e4610a fix: log db-schemas and db-extra-search-path in schema cache load error (#4108) 2025-05-30 20:56:04 +02:00
Laurence IslaandWolfgang Walther 8531c502d7 docs: JWK kid parameter validation 2025-05-30 14:35:58 +02:00
Taimoor ZaeemandWolfgang Walther 0ba47180ae fix: allow db-extra-search-path to accept empty value 2025-05-30 14:35:57 +02:00
Taimoor ZaeemandWolfgang Walther b77605e0d3 refactor: remove unused optValue function from Config.hs module 2025-05-30 14:35:54 +02:00
steve-chavez 800b32a59c docs: remove Greenplum integration
They're not really a sponsor, so it's not fair to include them.
2025-05-28 17:44:00 -05:00
Wolfgang Walther c48b6bc85b docs: fix functions link in api/preferences
External link syntax was used instead of internal reference.
2025-05-28 09:15:50 +02:00
Wolfgang Walther f899957675 docs: apply redirects
Those redirect, so we might as well hardcode the them.
2025-05-28 09:15:50 +02:00
Wolfgang Walther 609289d2bb docs: remove outdated "in production" links
Moat has been bought by Oracle. The advertising subpage redirects to
oracle.com, but pointing at that would be a bit misleading.

No need to keep failing links either.
2025-05-28 09:15:47 +02:00
steve-chavez 9e58946ec6 docs: update architecture HTTP link
It links directly to warp page, which is confusing. Link to the
same page reference instead, which finally links to warp.
2025-05-26 13:45:33 -05:00
Taimoor Zaeemandsteve-chavez 48a7b8dee7 docs: explain use of max-affected preference with rpc 2025-05-22 12:49:53 -05:00
Wolfgang Walther 23a4573a9c docs: Update sphinx-rtd-theme to 3.0.2 2025-05-22 08:07:54 +02:00
Taimoor ZaeemandWolfgang Walther 91814cd4f8 docs: add note in JWT Role Extraction section
Add a note describing that the used JSPath DSL does not
strictly follow the JSONPath as described in RFC 9535
2025-05-21 21:02:00 +02:00
steve-chavez 8ef5263b04 docs: add warning for duplicate keys in spread 2025-05-15 17:42:47 -05:00
steve-chavez a71f938a44 docs: clarify spread join table 2025-05-15 15:51:14 -05:00
steve-chavez e6d4bfa465 docs: clarify fts 2025-05-15 11:57:23 -05:00
steve-chavez b90d26034e docs: clarify spread feature 2025-05-15 11:17:59 -05:00
Taimoor ZaeemandWolfgang Walther 0230a844b2 test: add test for duplicate entries in pg_description with same OID 2025-05-14 21:44:23 +02:00
Taimoor ZaeemandWolfgang Walther e0e60fa433 fix: jwt error return status 400 for invalid role (#4081) 2025-05-14 21:44:22 +02:00
Taimoor ZaeemandWolfgang Walther 7269630538 test: add test when using .aud in jwt-role-claim-key 2025-05-14 21:44:20 +02:00
Laurence Isla cc2550a58b docs: fix link to SQL Query Logs 2025-05-09 20:59:31 -05:00
steve-chavez e20dc60e83 docs: jwt role extraction link to config
The feature section was missing a link to its config.

Also shorten the section name.
2025-05-09 20:47:30 -05:00
Wolfgang Walther 89fb2878df chore: adjust changelog for db-extra-search-path
Came up in #4073
2025-05-09 14:57:24 +02:00
258 changed files with 4775 additions and 11629 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
freebsd_instance: freebsd_instance:
image_family: freebsd-14-3 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
@@ -35,7 +35,7 @@ build_task:
- find main src -type f -iname '*.hs' -exec md5sum "{}" + - find main src -type f -iname '*.hs' -exec md5sum "{}" +
build_script: | build_script: |
stack build -j 1 --local-bin-path . --copy-bins stack build -j 1 --local-bin-path . --copy-bins --stack-yaml stack-21.7.yaml
strip postgrest strip postgrest
bin_artifacts: bin_artifacts:
-2
View File
@@ -1,2 +0,0 @@
# Ignore blame for commit that moved protolude files under src/protolude
d4949c633e8172d0e4dd8f5c991eaaae6b48fbb0
+55
View File
@@ -0,0 +1,55 @@
# Contributing to PostgREST
**First:** if you're unsure or afraid of _anything_, just ask or
submit the issue or pull request anyways. You won't be yelled at
for giving your best effort. The worst that can happen is that
you'll be politely asked to change something. We appreciate any
sort of contributions, and don't want a wall of rules to get in the
way of that.
However, for those individuals who want a bit more guidance on the
best way to contribute to the project, read on. This document will
cover what we're looking for. By addressing all the points we're
looking for, it raises the chances we can quickly merge or address
your contributions.
## Issues
For questions on how to use PostgREST, please use
[GitHub discussions](https://github.com/PostgREST/postgrest/discussions).
### Reporting an Issue
* Make sure you test against the latest [stable release](https://github.com/PostgREST/postgrest/releases/latest)
and also against the latest [nightly release](https://github.com/PostgREST/postgrest/releases/tag/nightly).
It is possible we already fixed the bug you're experiencing.
* Provide steps to reproduce the issue, including your OS version and
the specific database schema that you are using.
* Please include SQL logs for issues involving runtime problems. To obtain logs first
[enable logging all statements](http://www.microhowto.info/howto/log_all_queries_to_a_postgresql_server.html),
then [find your logs](http://blog.endpoint.com/2014/11/dear-postgresql-where-are-my-logs.html).
* If your database schema has changed while the PostgREST server is running,
[send the server a `SIGUSR1` signal](http://postgrest.org/en/latest/admin.html#schema-reloading) or restart it to ensure the schema cache
is not stale. This sometimes fixes apparent bugs.
## Code
We have a fully nix-based development environment with many tools for a smooth development workflow available.
Check the [development docs](https://github.com/PostgREST/postgrest/blob/main/nix/README.md) on how to set it up and use it.
### Haskell Conventions
* All contributions must pass the tests before being merged. When
you create a pull request your code will automatically be tested.
* All code must also pass [hlint](http://community.haskell.org/~ndm/hlint/) and [stylish-haskell](https://github.com/jaspervdj/stylish-haskell)
with no warnings. This helps enforce a uniform style for all committers. Continuous integration will check this as well on every
pull request. There are useful tools in the nix-shell that help with checking this locally. You can run `postgrest-check` to do this manually but
we recommend adding it to `.git/hooks/pre-commit` as `nix-shell --run postgrest-check` to automatically check this before doing a commit.
### Running Tests
For instructions on running tests, see the [development docs](https://github.com/PostgREST/postgrest/blob/main/nix/README.md#testing).
+11 -12
View File
@@ -4,18 +4,17 @@ When submitting a new feature or fix:
- Add a new entry to the CHANGELOG - https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md#unreleased - Add a new entry to the CHANGELOG - https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md#unreleased
- If relevant, update the docs - If relevant, update the docs
- Use a prefix for the PR title or commits, e.g. "fix: description of the fix". - Use a prefix for the PR title or commits, e.g. "fix: description of the fix".
+ `add`, Add a new feature + `fix`, bug fixes
+ `amend`, To amend an unrealease commit + `feat`, new features added
+ `change`, Breaking changes + `perf`, performance improvements
+ `chore`, Maintenance, update sponsors, changelog, readme etc + `docs`, updating the documentation
+ `ci`, CI configuration files and scripts + `nix`, related to the Nix development environment
+ `docs`, Documentation + `ci`, related to the Continuous Integration modules
+ `fix`, Bug fix + `test`, related to the testing modules
+ `nix`, Related to Nix + `refactor`, refactoring code
+ `perf`, Performance improvements + `deprecate`, deprecating a feature
+ `refactor`, Refactoring code + `changelog`, updating the CHANGELOG
+ `remove`, Remove a feature or fix + `chore`, maintenance (build process, updating sponsors, etc.)
+ `test`, Adding tests
+ Other prefixes may be used if necessary + Other prefixes may be used if necessary
- If there's a breaking change, add `BREAKING CHANGE` and an explanation to your commit message - If there's a breaking change, add `BREAKING CHANGE` and an explanation to your commit message
--> -->
+5
View File
@@ -0,0 +1,5 @@
# TODO: Remove this once a new actionlint release has been cut
# and made its way to us through nixpkgs.
self-hosted-runner:
labels:
- ubuntu-24.04-arm
@@ -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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 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 }}
+7 -6
View File
@@ -8,6 +8,7 @@ inputs:
required: true required: true
save-prs: save-prs:
description: Whether to additionally store the cache in a pull request, too. Should only be used for very small caches. description: Whether to additionally store the cache in a pull request, too. Should only be used for very small caches.
type: boolean
prefix: prefix:
description: Cache key prefix to be used in both primary key and restore-keys. description: Cache key prefix to be used in both primary key and restore-keys.
required: true required: true
@@ -18,17 +19,17 @@ inputs:
runs: runs:
using: composite using: composite
steps: steps:
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - 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 }}-${{ runner.arch }}-${{ inputs.prefix }}-${{ inputs.suffix }} key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
restore-keys: | restore-keys: |
${{ runner.os }}-${{ runner.arch }}-${{ inputs.prefix }}- ${{ runner.os }}-${{ inputs.prefix }}-
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - 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 }}
key: ${{ runner.os }}-${{ runner.arch }}-${{ inputs.prefix }}-${{ inputs.suffix }} key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
restore-keys: | restore-keys: |
${{ runner.os }}-${{ runner.arch }}-${{ inputs.prefix }}- ${{ runner.os }}-${{ inputs.prefix }}-
+2 -2
View File
@@ -11,12 +11,12 @@ inputs:
runs: runs:
using: composite using: composite
steps: steps:
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34 - 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@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: postgrest name: postgrest
authToken: ${{ inputs.authToken }} authToken: ${{ inputs.authToken }}
-3
View File
@@ -4,9 +4,6 @@ codecov:
comment: false comment: false
github_checks:
annotations: true
coverage: coverage:
status: status:
project: project:
+19 -7
View File
@@ -3,7 +3,7 @@
"extends": [ "extends": [
"config:best-practices" "config:best-practices"
], ],
"baseBranchPatterns": [ "baseBranches": [
"main", "main",
"/^v[0-9]+/" "/^v[0-9]+/"
], ],
@@ -13,16 +13,28 @@
}, },
"packageRules": [ "packageRules": [
{ {
"matchManagers": [ "matchBaseBranches": [ "/^v[0-9]+/" ],
"haskell-cabal" "matchManagers": ["haskell-cabal"],
],
"enabled": false "enabled": false
}, },
{ {
"matchBaseBranches": [ "matchBaseBranches": [ "/^v[0-9]+/" ],
"/^v[0-9]+/"
],
"groupName": "all dependencies" "groupName": "all dependencies"
},
{
"matchManagers": ["haskell-cabal"],
"matchPackageNames": ["base", "bytestring", "containers", "directory", "mtl", "parsec", "process", "text"],
"groupName": "GHC dependencies"
},
{
"matchManagers": ["haskell-cabal"],
"matchPackageNames": ["hasql", "hasql-dynamic-statements", "hasql-notifications", "hasql-transaction", "hasql-pool"],
"groupName": "hasql"
},
{
"matchManagers": ["haskell-cabal"],
"matchPackageNames": ["fuzzyset"],
"allowedVersions": "<0.3"
} }
] ]
} }
-52
View File
@@ -1,52 +0,0 @@
name: Backport
on:
pull_request_target:
types:
- closed
- labeled
jobs:
backport:
name: Backport
runs-on: ubuntu-24.04
# It triggers only when PR is already merged on either:
#
# - The merge event itself (action != labeled) or
# - A label event with the right label (backport ...).
#
# The result will be that we can add the label before or after merge,
# but the workflow will only run once the PR had been merged.
if: >
github.event.pull_request.merged &&
(
github.event.action != 'labeled' ||
startsWith(github.event.label.name, 'backport')
)
steps:
# This actions creates the github token using the postgrest app secrets
- name: Create Github App Token
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ vars.POSTGREST_CI_APP_ID }}
private-key: ${{ secrets.POSTGREST_CI_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
permission-workflows: write # required when backporting CI changes
# This is required for backport action to cherry-pick the PR
- name: Fetch PR ref
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
token: ${{ steps.app-token.outputs.token }}
# Backport action that creates the PR with given settings
- name: Create backport PR
uses: korthout/backport-action@7c3f6cd5843cac11bc59a04a1b7699af93261670 # v4.5
with:
github_token: ${{ steps.app-token.outputs.token }}
pull_description: 'Backport for #${pull_number}.'
pull_title: '${target_branch}: ${pull_title}'
+42 -39
View File
@@ -16,7 +16,6 @@ on:
- .github/* - .github/*
- '*.nix' - '*.nix'
- nix/** - nix/**
- flake.lock
- .cirrus.yml - .cirrus.yml
- cabal.project* - cabal.project*
- postgrest.cabal - postgrest.cabal
@@ -31,61 +30,56 @@ concurrency:
jobs: jobs:
static: static:
strategy: name: Nix - Linux x86-64 static
fail-fast: false runs-on: ubuntu-24.04
matrix:
include:
- name: Linux aarch64
runs-on: ubuntu-24.04-arm
artifact: aarch64
- name: Linux x86-64
runs-on: ubuntu-24.04
artifact: x86-64
name: Nix - ${{ matrix.name }} static
runs-on: ${{ matrix.runs-on }}
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build static executable - name: Build static executable
run: nix-build -A postgrestStatic -A postgrestStatic.tests run: nix-build -A postgrestStatic
- name: Save built executable as artifact - name: Save built executable as artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: postgrest-linux-static-${{ matrix.artifact }} name: postgrest-linux-static-x86-64
path: result/bin/postgrest path: result/bin/postgrest
if-no-files-found: error if-no-files-found: error
- name: Build Docker image - name: Build Docker image
run: nix-build -A docker.image --out-link postgrest-docker-${{ matrix.artifact }}.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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: postgrest-docker-${{ matrix.artifact }} name: postgrest-docker-x86-64
path: postgrest-docker-${{ matrix.artifact }}.tar.gz path: postgrest-docker.tar.gz
if-no-files-found: error if-no-files-found: error
macos: macos:
name: Nix - MacOS name: Nix - MacOS
runs-on: macos-15 runs-on: macos-14
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Install nix-build-uncached - name: Install gnu sed
run: nix-env -f default.nix -iA nix-build-uncached run: brew install gnu-sed
- name: Build everything (default.nix) - name: Build everything
run: nix-build-uncached run: |
# The --dry-run will give us a list of derivations to download from cachix and
- name: Build everything (shell.nix) # derivations to build. We only take those that would have to be built and then build
run: nix-build-uncached shell.nix # those explicitly. This has the advantage that pure verification will not include
# a download anymore, making it much faster. If something needs to be built, only
# the dependencies required to do so will be downloaded, but not everything.
nix-build --dry-run 2>&1 \
| gsed -e '1,/derivations will be built:$/d' -e '/paths will be fetched/Q' \
| xargs nix-build
stack: stack:
@@ -93,6 +87,15 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- name: Linux aarch64
runs-on: ubuntu-24.04-arm
cache: |
~/.stack/pantry
~/.stack/snapshots
~/.stack/stack.sqlite3
artifact: postgrest-ubuntu-aarch64
deps: sudo apt-get update && sudo apt-get install libpq-dev
- name: MacOS aarch64 - name: MacOS aarch64
runs-on: macos-14 runs-on: macos-14
cache: | cache: |
@@ -103,7 +106,7 @@ jobs:
deps: brew link --force libpq deps: brew link --force libpq
- name: MacOS x86-64 - name: MacOS x86-64
runs-on: macos-15-intel runs-on: macos-13
cache: | cache: |
~/.stack/pantry ~/.stack/pantry
~/.stack/snapshots ~/.stack/snapshots
@@ -123,11 +126,11 @@ jobs:
name: Stack - ${{ matrix.name }} name: Stack - ${{ matrix.name }}
runs-on: ${{ matrix.runs-on }} runs-on: ${{ matrix.runs-on }}
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0 - uses: haskell-actions/setup@64445b6b5dd545faf5f8e2acee8253eb5c2b29aa # v2.7.11
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.10.3 ghc-version: 9.6.6
enable-stack: true enable-stack: true
stack-no-global: true stack-no-global: true
stack-setup-ghc: true stack-setup-ghc: true
@@ -152,7 +155,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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: ${{ matrix.artifact }} name: ${{ matrix.artifact }}
path: | path: |
@@ -165,7 +168,7 @@ jobs:
name: Stack - FreeBSD from CirrusCI name: Stack - FreeBSD from CirrusCI
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: ./.github/actions/artifact-from-cirrus - uses: ./.github/actions/artifact-from-cirrus
with: with:
token: ${{ github.token }} token: ${{ github.token }}
@@ -177,13 +180,13 @@ jobs:
cabal: cabal:
strategy: strategy:
matrix: matrix:
ghc: ['9.10.3', '9.12.3'] ghc: ['9.6.6', '9.8.2']
fail-fast: false fail-fast: false
name: Cabal - Linux x86-64 - GHC ${{ matrix.ghc }} name: Cabal - Linux x86-64 - GHC ${{ matrix.ghc }}
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0 - uses: haskell-actions/setup@64445b6b5dd545faf5f8e2acee8253eb5c2b29aa # v2.7.11
with: with:
ghc-version: ${{ matrix.ghc }} ghc-version: ${{ matrix.ghc }}
- name: Cache .cabal - name: Cache .cabal
+1 -22
View File
@@ -20,7 +20,7 @@ jobs:
name: Lint & Style name: Lint & Style
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
@@ -30,24 +30,3 @@ jobs:
run: postgrest-lint run: postgrest-lint
- name: Run style check (auto-format with `nix-shell --run postgrest-style`) - name: Run style check (auto-format with `nix-shell --run postgrest-style`)
run: postgrest-style-check run: postgrest-style-check
commit:
if: github.event_name != 'push' # we don't run this on a push, a failure on push disrupts the release workflow
name: Commit
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 100 # fetch history (last 100 commits) instead of default shallow clone history, this is deemed enough for a PR history
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
tools: gitTools.commitCheck.bin
- name: Run commitlint (check locally with `nix-shell --run postgrest-commitlint`)
run: |
# Fetch target branch explicitly
git fetch origin ${{ github.base_ref }}
# Run commitlint
postgrest-commitlint --from origin/${{ github.base_ref }} --to HEAD
+2 -2
View File
@@ -50,14 +50,14 @@ jobs:
- test - test
- build - build
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
ssh-key: ${{ secrets.POSTGREST_SSH_KEY }} ssh-key: ${{ secrets.POSTGREST_SSH_KEY }}
- name: Tag latest commit - name: Tag latest commit
run: | run: |
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)" cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
if [[ "$cabal_version" == *.* ]]; then if [[ "$cabal_version" == *.*.* ]]; then
git fetch --tags git fetch --tags
if [ -z "$(git tag --list "v$cabal_version")" ]; then if [ -z "$(git tag --list "v$cabal_version")" ]; then
+2 -3
View File
@@ -14,7 +14,6 @@ on:
- .github/actions/setup-nix/** - .github/actions/setup-nix/**
- default.nix - default.nix
- nix/** - nix/**
- flake.lock
- docs/** - docs/**
- '!**.md' - '!**.md'
@@ -28,7 +27,7 @@ jobs:
name: Build name: Build
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
@@ -42,7 +41,7 @@ jobs:
name: Spellcheck name: Spellcheck
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
+1 -2
View File
@@ -3,13 +3,12 @@ name: Linkcheck
on: on:
schedule: schedule:
- cron: '1 2 * * 3' - cron: '1 2 * * 3'
workflow_dispatch:
jobs: jobs:
linkcheck: linkcheck:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
+41 -33
View File
@@ -26,7 +26,7 @@ jobs:
needs: needs:
- build - build
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Check the version to be released - name: Check the version to be released
run: | run: |
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)" cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
@@ -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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: release-changes name: release-changes
path: CHANGES.md path: CHANGES.md
@@ -64,9 +64,9 @@ jobs:
needs: needs:
- prepare - prepare
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Download all artifacts - name: Download all artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with: with:
path: artifacts path: artifacts
- name: Create release bundle with archives for all builds - name: Create release bundle with archives for all builds
@@ -75,9 +75,6 @@ jobs:
mkdir -p release-bundle mkdir -p release-bundle
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-linux-static-aarch64.tar.xz" \
-C artifacts/postgrest-linux-static-aarch64 postgrest
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-linux-static-x86-64.tar.xz" \ tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-linux-static-x86-64.tar.xz" \
-C artifacts/postgrest-linux-static-x86-64 postgrest -C artifacts/postgrest-linux-static-x86-64 postgrest
@@ -90,11 +87,14 @@ jobs:
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-freebsd-x86-64.tar.xz" \ tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-freebsd-x86-64.tar.xz" \
-C artifacts/postgrest-freebsd-x86-64 postgrest -C artifacts/postgrest-freebsd-x86-64 postgrest
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-ubuntu-aarch64.tar.xz" \
-C artifacts/postgrest-ubuntu-aarch64 postgrest
zip --junk-paths "release-bundle/postgrest-${GITHUB_REF_NAME}-windows-x86-64.zip" \ zip --junk-paths "release-bundle/postgrest-${GITHUB_REF_NAME}-windows-x86-64.zip" \
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: release-bundle name: release-bundle
path: release-bundle path: release-bundle
@@ -138,42 +138,50 @@ jobs:
env: env:
DOCKER_REPO: ${{ vars.DOCKER_REPO }} DOCKER_REPO: ${{ vars.DOCKER_REPO }}
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Download aarch64 Docker image
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: postgrest-docker-aarch64
- name: Download x86-64 Docker image - name: Download x86-64 Docker image
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with: with:
name: postgrest-docker-x86-64 name: postgrest-docker-x86-64
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Download aarch64 binary
- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: postgrest-ubuntu-aarch64
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.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 }}
- name: Build aarch64 Docker image
run: |
# This only pushes the image via digest, not a tag. This will not appear
# in the image list on Docker Hub, yet. It will be later added to the main
# tag's manifest.
docker buildx build \
-t "$DOCKER_REPO/postgrest" \
--platform linux/arm64 \
--output push-by-digest=true,type=image,push=true \
--metadata-file metadata.json \
.
echo "SHA256_ARM=$(jq -r '."containerimage.digest"' metadata.json)" >> "$GITHUB_ENV"
- name: Publish images on Docker Hub - name: Publish images on Docker Hub
run: | run: |
docker load -i postgrest-docker-aarch64.tar.gz docker load -i postgrest-docker.tar.gz
docker tag postgrest:latest "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-arm64"
docker push "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-arm64"
docker load -i postgrest-docker-x86-64.tar.gz docker tag postgrest:latest "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
docker tag postgrest:latest "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-amd64" docker push "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
docker push "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-amd64" docker buildx imagetools create --append \
-t "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}" \
docker manifest create "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}" \ "$DOCKER_REPO/postgrest@$SHA256_ARM"
"$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-arm64" \
"$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-amd64"
docker manifest push "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
# Only tag 'latest' for full releases # Only tag 'latest' for full releases
if [ "${GITHUB_REF_NAME}" != "devel" ]; then if [ "${GITHUB_REF_NAME}" != "devel" ]; then
echo "Pushing to 'latest' tag for full release of ${GITHUB_REF_NAME} ..." echo "Pushing to 'latest' tag for full release of ${GITHUB_REF_NAME} ..."
docker manifest create "$DOCKER_REPO/postgrest:latest" \ docker tag postgrest:latest "$DOCKER_REPO"/postgrest:latest
"$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-arm64" \ docker push "$DOCKER_REPO"/postgrest:latest
"$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-amd64" docker buildx imagetools create --append \
docker manifest push "$DOCKER_REPO/postgrest:latest" -t "$DOCKER_REPO/postgrest:latest" \
"$DOCKER_REPO/postgrest@$SHA256_ARM"
else else
echo "Skipping push to 'latest' tag for pre-release..." echo "Skipping push to 'latest' tag for pre-release..."
fi fi
@@ -186,8 +194,8 @@ jobs:
vars.DOCKER_REPO && vars.DOCKER_USER && vars.DOCKER_REPO && vars.DOCKER_USER &&
github.ref == 'refs/tags/devel' github.ref == 'refs/tags/devel'
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.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 }}
+23 -41
View File
@@ -17,7 +17,6 @@ on:
- .github/actions/setup-nix/** - .github/actions/setup-nix/**
- default.nix - default.nix
- nix/** - nix/**
- flake.lock
- .stylish-haskell.yaml - .stylish-haskell.yaml
- cabal.project - cabal.project
- postgrest.cabal - postgrest.cabal
@@ -40,19 +39,17 @@ jobs:
# https://github.com/actions/runner/issues/241#issuecomment-842566950 # https://github.com/actions/runner/issues/241#issuecomment-842566950
shell: script -qec "bash --noprofile --norc -eo pipefail {0}" shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
tools: tests.coverage.bin tests.testDoctests.bin tests.testSpecIdempotence.bin cabalTools.update.bin tools: tests.coverage.bin tests.testDoctests.bin tests.testSpecIdempotence.bin
- run: postgrest-cabal-update - name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
- name: Run coverage (IO tests and Spec tests against latest supported PostgreSQL)
run: postgrest-coverage run: postgrest-coverage
- name: Upload coverage to codecov - name: Upload coverage to codecov
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
with: with:
files: ./coverage/codecov.json files: ./coverage/codecov.json
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
@@ -70,7 +67,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
pgVersion: [14, 15, 16, 17, 18] pgVersion: [12, 13, 14, 15, 16, 17]
name: PG ${{ matrix.pgVersion }} name: PG ${{ matrix.pgVersion }}
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
defaults: defaults:
@@ -79,45 +76,36 @@ jobs:
# https://github.com/actions/runner/issues/241#issuecomment-842566950 # https://github.com/actions/runner/issues/241#issuecomment-842566950
shell: script -qec "bash --noprofile --norc -eo pipefail {0}" shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
tools: tests.testSpec.bin tests.testObservability.bin tests.testIO.bin tests.testBigSchema.bin withTools.pg-${{ matrix.pgVersion }}.bin cabalTools.update.bin tools: tests.testSpec.bin tests.testIO.bin tests.testBigSchema.bin withTools.postgresql-${{ matrix.pgVersion }}.bin
- run: postgrest-cabal-update
- name: Run spec tests - name: Run spec tests
if: always() if: always()
run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-spec run: postgrest-with-postgresql-${{ matrix.pgVersion }} postgrest-test-spec
- name: Run observability tests
if: always()
run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-observability
- name: Run IO tests - name: Run IO tests
if: always() if: always()
run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-io -vv run: postgrest-with-postgresql-${{ matrix.pgVersion }} postgrest-test-io -vv
- name: Run IO tests on a big schema - name: Run IO tests on a big schema
if: always() if: always()
run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-big-schema -vv run: postgrest-with-postgresql-${{ matrix.pgVersion }} postgrest-test-big-schema -vv
memory: memory:
name: Memory name: Memory
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
tools: tests.testMemory.bin cabalTools.update.bin tools: tests.testMemory.bin
- run: postgrest-cabal-update
- name: Run memory tests - name: Run memory tests
run: postgrest-test-memory run: postgrest-test-memory
@@ -125,46 +113,40 @@ jobs:
loadtest: loadtest:
strategy: strategy:
matrix: matrix:
kind: ['mixed', 'errors', 'jwt-hs', 'jwt-hs-cache', 'jwt-hs-cache-worst', 'jwt-rsa', 'jwt-rsa-cache', 'jwt-rsa-cache-worst'] kind: ['mixed', 'jwt']
name: Loadtest name: Loadtest
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
tools: loadtest.loadtestAgainst.bin loadtest.report.bin cabalTools.update.bin tools: loadtest.loadtestAgainst.bin loadtest.report.bin
- uses: WyriHaximus/github-action-get-previous-tag@04e8485ecb6487243907e330d522ff60f02283ce # v1.4.0
- run: postgrest-cabal-update id: get-latest-tag
with:
prefix: v
- name: Run loadtest - name: Run loadtest
env:
TARGET_BRANCH: ${{ github.base_ref || github.ref_name }}
run: | run: |
if [ "$TARGET_BRANCH" = "main" ]; then postgrest-loadtest-against -k ${{ matrix.kind }} main ${{ steps.get-latest-tag.outputs.tag }}
latest_tag=$(git tag --sort=-creatordate --list "v*" | head -n1) postgrest-loadtest-report >> "$GITHUB_STEP_SUMMARY"
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 -g ${{ matrix.kind }} >> "$GITHUB_STEP_SUMMARY"
flake: flake:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
runs-on: runs-on:
- macos-15-intel # x86_64-darwin - macos-13 # x86_64-darwin
- macos-14 # aarch64-darwin - macos-14 # aarch64-darwin
- ubuntu-24.04 # x86_64-linux - ubuntu-24.04 # x86_64-linux
- ubuntu-24.04-arm # aarch64-linux - ubuntu-24.04-arm # aarch64-linux
name: Flake Check name: Flake Check
runs-on: ${{ matrix.runs-on }} runs-on: ${{ matrix.runs-on }}
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Nix Environment - name: Setup Nix Environment
-2
View File
@@ -25,5 +25,3 @@ loadtest
.history .history
.docs-build .docs-build
gen_targets.http gen_targets.http
gen_jwk.json
gen_private.json
+1 -1
View File
@@ -7,4 +7,4 @@ python:
build: build:
os: ubuntu-24.04 os: ubuntu-24.04
tools: tools:
python: "3.12" python: "3.11"
+10 -15
View File
@@ -13,26 +13,26 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
</a> </a>
</td> </td>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank"> <a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/supabase.svg"> <img width="296px" src="static/neon.jpg">
</a> </a>
</td> </td>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://www.euronodes.com/postgrest" target="_blank"> <a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/euronodes.svg"> <img width="296px" src="static/code-build.png">
</a> </a>
</td> </td>
</tr> </tr>
<tr></tr> <tr></tr>
<tr> <tr>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank"> <a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/neon.jpg"> <img width="296px" src="static/tembo.png">
</a> </a>
</td> </td>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank"> <a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
<img width="296px" src="static/bytebase.svg"> <img width="296px" src="static/supabase.png">
</a> </a>
</td> </td>
</tr> </tr>
@@ -89,13 +89,8 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
</a> </a>
</td> </td>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank"> <a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="222px" src="static/code-build.png"> <img width="222px" src="static/gnuhost.png">
</a>
</td>
<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> </a>
</td> </td>
</tr> </tr>
+4 -187
View File
@@ -1,190 +1,10 @@
# Change Log # Change Log
All notable changes to this project will be documented in this file. From version `14.0` onwards PostgREST follows a `MAJOR.PATCH` two-part versioning. Only even-numbered MAJOR versions will be released, reserving odd-numbered MAJOR versions for development. All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased ## Unreleased
### Added
- Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359
- Add string slicing operator for `jwt-role-claim-key` by @taimoorzaeem in #4599
- Optimize requests with `Prefer: count=exact` that do not use ranges or `db-max-rows` by @laurenceisla in #3957
+ Removed unnecessary double count when building the `Content-Range`.
- Add config `client-error-verbosity` to customize error verbosity by @taimoorzaeem in #4088, #3980, #3824
- Add `Vary` header to responses by @develop7 in #4609
- Add config `db-timezone-enabled` for optional querying of timezones by @taimoorzaeem in #4751
- Log schema cache queries timings on `log-level=debug` by @steve-chavez in #4805
### Fixed
- Shutdown should wait for in flight requests by @mkleczek in #4702
- Fix login with uppercase and mixed case role names by @taimoorzaeem in #4678
- Remove automatic transaction retries on `40001 (serialization_failure)` errors to prevent replication lag by @laurenceisla in #3673
- Fix unexpected results when embedding and filtering the same table more than once by @laurenceisla in #4075
### Changed
- Drop support for PostgreSQL EOL version 13 by @wolfgangwalther in #4193
- All responses now include a `Vary` header by @develop7 in #4609
- Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359
+ Now fails at startup. Prior to this, it failed with `PGRST205` on requests related to these schemas.
- Build a static executable for aarch64-linux by @wolfgangwalther in #4193
- Build the minimal docker image for aarch64-linux by @wolfgangwalther in #4193
- The name of an embedded table can no longer be used in filters if it has an alias by @laurenceisla in #4075
+ e.g. `?select=alias:table(*)&table.id=eq.1` is not possible anymore, use `?select=alias:table(*)&alias.id=eq.1` instead.
## [14.10] - 2026-04-16
### Added
- Log when the pool is released during schema cache reload on `log-level=debug` by @mkleczek in #4668
### Fixed
- Fix unnecessary connection pool flushes during schema cache reloading by @mkleczek in #4645
## [14.9] - 2026-04-10
### Added
- Log host, port and pg version of listener database connection by @mkleczek in #4617 #4618
### Fixed
- Remove red herring warp logs on default log-level, only emit them on `log-level=debug` by @steve-chavez in #4799
## [14.8] - 2026-04-03
### Added
- Log a `HINT` when the LISTEN channel stops working due to a PostgreSQL bug by @laurenceisla in #4581
### Fixed
- Fix invalid OpenAPI 2.0 format for integer types (`smallint`, `integer`, `bigint`) by @arturbent0 in #4641
## [14.7] - 2026-03-20
### Fixed
- Fix not logging SIGTERM and SIGINT by @steve-chavez in #4728
## [14.6] - 2026-03-06
### Fixed
- Fix leaking table and function names when calculating error hint by @taimoorzaeem in #4675
## [14.5] - 2026-02-12
### Fixed
- Don't hide async exceptions in logs by @stevechavez in #4646
## [14.4] - 2026-01-29
### Fixed
- Ensure Listener connections are released by @mkleczek in #4614
- Fix incorrectly filtering the returned representation for PATCH requests when using `or/and` filters by @laurenceisla in #3707
- Fix listener running with exception masked after first failure by @mkleczek in #4615
## [14.3] - 2026-01-03
### Fixed
- Fix performance and high memory usage of relation hint calculation by @mkleczek in #4462, #4463
## [14.2] - 2025-12-18
### Fixed
- Fix `hasSingleUnnamedParam` incorrectly matching functions with named parameters by @joelonsql in #4553
+ Functions with a single named parameter (e.g., `foo(data json)`) no longer incorrectly match the single-param fallback, returning a clean `PGRST202` error instead of a confusing PostgreSQL `42883` error.
- Fix misleading logs on unsupported PostgreSQL versions by @taimoorzaeem in #4519
- Fix regression where the `PGRST103` error response was truncated by @laurenceisla in #4455
+ Happened when an `offset` was greater than the rows requested and `Prefer: count=exact` was sent.
- Fix not returning `Content-Length` on empty HTTP `201` responses by @laurenceisla in #4518
- Fix inaccurate Server-Timing header durations by @steve-chavez in #4522
- Fix inaccurate "Schema cache queried" logs by @steve-chavez in #4522
## [14.1] - 2025-11-05
## Fixed
- Fix `db-pre-config` function failing when function names are pg reserved words by @taimoorzaeem in #4380
- Fix `server-host=!6` incorrectly binds to IPv4 address by @taimoorzaeem in #3202
## [14.0] - 2025-10-24
### Added
- Bounded JWT cache using the SIEVE algorithm by @mkleczek in #4084
+ It now uses a fixed size cache instead of arbitrary sized cache.
- Add `--ready` flag for postgrest healthcheck by @taimoorzaeem in #4239
### Fixed
- Fix not logging OpenAPI queries when `log-query=main-query` is enabled by @steve-chavez in #4226
- Fix not logging explain query when `log-query=main-query` is enabled by @steve-chavez in #4319
- Fix not logging transaction variables and db-pre-request function when `log-query=main-query` is enabled by @steve-chavez in #3934
- Fix not logging the JSON message to stderr on a `PGRST002` error by @laurenceisla in #4129
- Fix reloading the Schema Cache unnecessarily on a `PGRST002` error by @laurenceisla in #4367
- Fix schema cache loading taking a long time for large schemas by @mkleczek in #4360, #3704
### Changed
- Drop support for PostgreSQL EOL version 12 by @wolfgangwalther in #3865
- From now on PostgREST will follow a `MAJOR.PATCH` two-part versioning. Only even-numbered MAJOR versions will be released, reserving odd-numbered MAJOR versions for development.
- Replaced `jwt-cache-max-lifetime` config with `jwt-cache-max-entries` by @mkleczek in #4084
- `log-query` config now takes a boolean instead of a string value by @steve-chavez in #3934
## [13.0.8] - 2025-10-24
### Fixed
- Fix loading utf-8 config files with `ASCII` locale set by @taimoorzaeem in #4386
## [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 #4080
- 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
- Fix `max-affected` preference not failing with RPC when `handling=strict` by @taimoorzaeem in #4100
- Fix a property definition's type in OpenAPI not showing the correct base type of a recursive domain by @laurenceisla in #4136
### Fixed
## [13.0.2] - 2025-06-02 ## [13.0.2] - 2025-06-02
### Fixed ### Fixed
@@ -252,9 +72,6 @@ All notable changes to this project will be documented in this file. From versio
- #3956, Drop `/config` endpoint of admin server - @steve-chavez - #3956, Drop `/config` endpoint of admin server - @steve-chavez
+ The endpoint was at risk of being left unprotected when exposing it. + The endpoint was at risk of being left unprotected when exposing it.
+ The accompanying `admin-server-config-enabled` config was also dropped. + The accompanying `admin-server-config-enabled` config was also dropped.
- #3598, PostgREST now validates the `kid` parameter of the JWT - @wolfgangwalther
+ If the JWT contains a ``kid`` parameter, then PostgREST will look for the JSON Web Key in the `jwt-secret`.
+ If the JWT doesn't contain a `kid`, the behavior should be backwards compatible. PostgREST will try each key in the `jwt-secret` one by one until it finds one that works.
- #3697, #3602, Querying non-existent table now returns `PGRST205` error instead of empty json - @taimoorzaeem - #3697, #3602, Querying non-existent table now returns `PGRST205` error instead of empty json - @taimoorzaeem
- #3600, #3926, Improve JWT errors - @taimoorzaeem - #3600, #3926, Improve JWT errors - @taimoorzaeem
+ Return `PGRST301` error when `Bearer` in auth header is sent empty + Return `PGRST301` error when `Bearer` in auth header is sent empty
@@ -691,7 +508,7 @@ All notable changes to this project will be documented in this file. From versio
### Added ### Added
- #1933, #2109, Add a minimal health check endpoint - @steve-chavez - #1933, #2109, Add a minimal health check endpoint - @steve-chavez
+ For enabling this, the `admin-server-port` config must be set explicitly + For enabling this, the `admin-server-port` config must be set explictly
+ A `<host>:<admin_server_port>/live` endpoint is available for checking if postgrest is running on its port/socket. 200 OK = alive, 503 = dead. + A `<host>:<admin_server_port>/live` endpoint is available for checking if postgrest is running on its port/socket. 200 OK = alive, 503 = dead.
+ A `<host>:<admin_server_port>/ready` endpoint is available for checking a correct internal state(the database connection plus the schema cache). 200 OK = ready, 503 = not ready. + A `<host>:<admin_server_port>/ready` endpoint is available for checking a correct internal state(the database connection plus the schema cache). 200 OK = ready, 503 = not ready.
- #1988, Add the current user to the request log on stdout - @DavidLindbom, @wolfgangwalther - #1988, Add the current user to the request log on stdout - @DavidLindbom, @wolfgangwalther
@@ -1174,7 +991,7 @@ All notable changes to this project will be documented in this file. From versio
- Customize content negotiation per route - @begriffs - Customize content negotiation per route - @begriffs
- Allow using nulls order without explicit order direction - @steve-chavez - Allow using nulls order without explicit order direction - @steve-chavez
- Fatal error on postgres unsupported version, format supported version in error message - @steve-chavez - Fatal error on postgres unsupported version, format supported version in error message - @steve-chavez
- Prevent database memory consumption by prepared statements caches - @ruslantalpa - Prevent database memory cosumption by prepared statements caches - @ruslantalpa
- Use specific columns in the RETURNING section - @ruslantalpa - Use specific columns in the RETURNING section - @ruslantalpa
- Fix columns alias for RETURNING - @steve-chavez - Fix columns alias for RETURNING - @steve-chavez
-62
View File
@@ -1,62 +0,0 @@
# Contributing to PostgREST
## AI Policy
We adhere to [Gentoo's AI policy](https://wiki.gentoo.org/wiki/Project:Council/AI_policy):
> It is expressly forbidden to contribute [...] any content that has been created with the assistance of Natural Language Processing artificial intelligence tools. This motion can be revisited, should a case been made over such a tool that does not pose copyright, ethical and quality concerns.
You can find more about its rationale [here](https://wiki.gentoo.org/wiki/Project:Council/AI_policy#Rationale).
## Issues
For questions on how to use PostgREST, please use
[GitHub discussions](https://github.com/PostgREST/postgrest/discussions).
### Reporting an Issue
* Make sure you test against the latest [stable release](https://github.com/PostgREST/postgrest/releases/latest)
and also against the latest [devel release](https://github.com/PostgREST/postgrest/releases/tag/devel).
It is possible we already fixed the bug you're experiencing.
* Provide steps to reproduce the issue, including your OS version and
the specific database schema that you are using.
* Please include SQL logs for issues involving runtime problems. To obtain logs first
[enable logging all statements](http://www.microhowto.info/howto/log_all_queries_to_a_postgresql_server.html),
then [find your logs](http://blog.endpoint.com/2014/11/dear-postgresql-where-are-my-logs.html).
* If your database schema has changed while the PostgREST server is running,
[send the server a `SIGUSR1` signal](http://postgrest.org/en/latest/admin.html#schema-reloading) or restart it to ensure the schema cache
is not stale. This sometimes fixes apparent bugs.
## Code
We have a fully nix-based development environment with many tools for a smooth development workflow available.
Check the [development docs](https://github.com/PostgREST/postgrest/blob/main/nix/README.md) on how to set it up and use it.
* All contributions must pass the tests before being merged. When
you create a pull request your code will automatically be tested.
* All fixes or features must have a test proving the improvement.
* All code must also pass a [linter](http://community.haskell.org/~ndm/hlint/) and [styler](https://github.com/jaspervdj/stylish-haskell)
with no warnings. This helps enforce a uniform style for all committers. Continuous integration will check this as well on every
pull request. There are useful tools in the nix-shell that help with checking this locally. You can run `postgrest-check` to do this manually but
we recommend adding it to `.git/hooks/pre-commit` as `nix-shell --run postgrest-check` to automatically check this before doing a commit.
### Running Tests
For instructions on running tests, see the [development docs](https://github.com/PostgREST/postgrest/blob/main/nix/README.md#testing).
### Structuring commits in pull requests
To simplify reviews, make it easy to split pull requests if deemed necessary, and to maintain clean and meaningful history of changes, you will be asked to update your PR if it does not follow the below rules:
* It must be possible to merge the PR branch into target using `git merge --ff-only`, ie. the source branch must be rebased on top of target.
* No merge commits in the source branch.
* All commits in the source branch must be self contained, meaning: it should be possible to treat each commit as a separate PR.
* Commits in the source branch must contain only related changes (related means the changes target a single problem/goal). For example, any refactorings should be isolated from the actual change implementation into separate commits.
* Tests, documentation, and changelog updates should be contained in the same commits as the actual code changes they relate to. An exception to this rule is when test or documentation changes are made in separate PR.
* Commit messages must be prefixed with one of the prefixes defined in [the list used by commit verification scripts](https://github.com/PostgREST/postgrest/blob/main/nix/tools/gitTools.nix#L11).
* Commit messages should contain a longer description of the purpose of the changes contained in the commit and, for non-trivial changes, a description of the changes themselves.
+21
View File
@@ -0,0 +1,21 @@
# PostgREST Docker Hub image for aarch64.
# 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
RUN apt-get update -y \
&& apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
COPY postgrest /usr/bin/postgrest
RUN chmod +x /usr/bin/postgrest
EXPOSE 3000
USER 1000
# Use the array form to avoid running the command using bash, which does not handle `SIGTERM` properly.
# See https://docs.docker.com/compose/faq/#why-do-my-services-take-10-seconds-to-recreate-or-stop
CMD ["postgrest"]
+2 -1
View File
@@ -1,4 +1,5 @@
Copyright (c) 2014-2026 The PostgREST contributors Copyright (c) 2014 Joe Nelson
Copyright (c) 2019 Steve Chavez
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the
+11 -15
View File
@@ -22,26 +22,26 @@ API than you are likely to write from scratch.
</a> </a>
</td> </td>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank"> <a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/supabase.svg"> <img width="296px" src="static/neon.jpg">
</a> </a>
</td> </td>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://www.euronodes.com/postgrest" target="_blank"> <a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/euronodes.svg"> <img width="296px" src="static/code-build.png">
</a> </a>
</td> </td>
</tr> </tr>
<tr></tr> <tr></tr>
<tr> <tr>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank"> <a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/neon.jpg"> <img width="296px" src="static/tembo.png">
</a> </a>
</td> </td>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank"> <a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
<img width="296px" src="static/bytebase.svg"> <img width="296px" src="static/supabase.png">
</a> </a>
</td> </td>
</tr> </tr>
@@ -52,8 +52,8 @@ Big thanks to our sponsors! You can join them by supporting PostgREST on [Patreo
## Usage ## Usage
1. See the docs for [how to install PostgREST on your platform](https://docs.postgrest.org/en/stable/explanations/install.html). You can also [use Docker](https://docs.postgrest.org/en/stable/explanations/install.html#docker). 1. Download the binary ([latest release](https://github.com/PostgREST/postgrest/releases/latest))
for your platform.
2. Invoke for help: 2. Invoke for help:
```bash ```bash
@@ -125,7 +125,7 @@ and limited with - range headers. More about
## Data Integrity ## Data Integrity
Rather than relying on an Object Relational Mapper and custom Rather than relying on an Object Relational Mapper and custom
imperative coding, this system requires you to put declarative constraints imperative coding, this system requires you put declarative constraints
directly into your database. Hence no application can corrupt your directly into your database. Hence no application can corrupt your
data (including your API server). data (including your API server).
@@ -142,10 +142,6 @@ You can help PostgREST ongoing maintenance and development by making a regular d
Every donation will be spent on making PostgREST better for the whole community. Every donation will be spent on making PostgREST better for the whole community.
## Contributing
Contributions are always welcome and appreciated. Please see the [Contributing guidelines](https://github.com/PostgREST/postgrest/blob/main/CONTRIBUTING.md).
## Thanks ## Thanks
The PostgREST organization is grateful to: The PostgREST organization is grateful to:
+2 -2
View File
@@ -1,4 +1,4 @@
packages: postgrest.cabal packages: postgrest.cabal
tests: true tests: true
allow-newer: package *
hasql:postgresql-libpq ghc-options: -split-sections
+1 -1
View File
@@ -1 +1 @@
index-state: hackage.haskell.org 2026-04-18T18:42:36Z index-state: hackage.haskell.org 2025-02-01T14:59:33Z
+14 -29
View File
@@ -1,6 +1,6 @@
{ system ? builtins.currentSystem { system ? builtins.currentSystem
, compiler ? "ghc9123" , compiler ? "ghc948"
, # Commit of the Nixpkgs repository that we want to use. , # Commit of the Nixpkgs repository that we want to use.
# It defaults to reading the inputs from flake.lock, which serves # It defaults to reading the inputs from flake.lock, which serves
@@ -43,7 +43,9 @@ let
allOverlays.build-toolbox allOverlays.build-toolbox
allOverlays.checked-shell-script allOverlays.checked-shell-script
allOverlays.gitignore allOverlays.gitignore
allOverlays.postgresql-libpq
(allOverlays.haskell-packages { inherit compiler; }) (allOverlays.haskell-packages { inherit compiler; })
allOverlays.slocat
]; ];
# Evaluated expression of the Nixpkgs repository. # Evaluated expression of the Nixpkgs repository.
@@ -52,17 +54,16 @@ let
postgresqlVersions = postgresqlVersions =
[ [
{ name = "pg-18"; postgresql = pkgs.postgresql_18.withPackages (p: [ p.postgis p.pg_safeupdate ]); } { name = "postgresql-17"; postgresql = pkgs.postgresql_17.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
{ name = "pg-17"; postgresql = pkgs.postgresql_17.withPackages (p: [ p.postgis p.pg_safeupdate ]); } { name = "postgresql-16"; postgresql = pkgs.postgresql_16.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
{ name = "pg-16"; postgresql = pkgs.postgresql_16.withPackages (p: [ p.postgis p.pg_safeupdate ]); } { name = "postgresql-15"; postgresql = pkgs.postgresql_15.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
{ name = "pg-15"; postgresql = pkgs.postgresql_15.withPackages (p: [ p.postgis p.pg_safeupdate ]); } { name = "postgresql-14"; postgresql = pkgs.postgresql_14.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
{ name = "pg-14"; postgresql = pkgs.postgresql_14.withPackages (p: [ p.postgis p.pg_safeupdate ]); } { name = "postgresql-13"; postgresql = pkgs.postgresql_13.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
{ name = "postgresql-12"; postgresql = pkgs.postgresql_12.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
]; ];
haskellPackages = pkgs.haskell.packages."${compiler}";
# Dynamic derivation for PostgREST # Dynamic derivation for PostgREST
postgrest = pkgs.lib.pipe (haskellPackages.callCabal2nix name src { }) [ postgrest = pkgs.lib.pipe (pkgs.haskell.packages."${compiler}".callCabal2nix name src { }) [
# To allow ghc-datasize to be used. # To allow ghc-datasize to be used.
lib.disableLibraryProfiling lib.disableLibraryProfiling
# We are never going to use dynamic haskell libraries anyway. "Dynamic" refers to how # We are never going to use dynamic haskell libraries anyway. "Dynamic" refers to how
@@ -74,7 +75,7 @@ let
# Options passed to cabal in dev tools and tests # Options passed to cabal in dev tools and tests
devCabalOptions = devCabalOptions =
"-f dev --test-show-detail=direct"; "-f dev --test-show-detail=direct --disable-shared";
inherit (pkgs.haskell) lib; inherit (pkgs.haskell) lib;
in in
@@ -83,14 +84,9 @@ rec {
# Derivation for the PostgREST Haskell package, including the executable, # Derivation for the PostgREST Haskell package, including the executable,
# libraries and documentation. We disable running the test suite on Nix # libraries and documentation. We disable running the test suite on Nix
# builds, as they require a database to be set up. We split the binary # builds, as they require a database to be set up.
# into a separate output, so that the default distribution via flake.nix postgrestPackage =
# has a much smaller closure size. lib.dontCheck postgrest;
postgrestPackage = pkgs.lib.pipe postgrest [
lib.dontCheck
lib.enableSeparateBinOutput
(haskellPackages.generateOptparseApplicativeCompletions [ "postgrest" ])
];
# Profiled dynamic executable. # Profiled dynamic executable.
postgrestProfiled = pkgs.lib.pipe postgrestPackage [ postgrestProfiled = pkgs.lib.pipe postgrestPackage [
@@ -107,9 +103,6 @@ rec {
inherit (pkgs.haskell.packages."${compiler}") ghcWithPackages; inherit (pkgs.haskell.packages."${compiler}") ghcWithPackages;
}; };
# Used by CI on MacOS
inherit (pkgs) nix-build-uncached;
### Tools ### Tools
cabalTools = cabalTools =
@@ -126,18 +119,10 @@ rec {
docs = docs =
pkgs.callPackage nix/tools/docs.nix { }; pkgs.callPackage nix/tools/docs.nix { };
# Git tools.
gitTools =
pkgs.callPackage nix/tools/gitTools.nix { };
# Load testing tools. # Load testing tools.
loadtest = loadtest =
pkgs.callPackage nix/tools/loadtest.nix { inherit withTools; }; pkgs.callPackage nix/tools/loadtest.nix { inherit withTools; };
# Utility for updating the pinned version of Nixpkgs.
nixpkgsTools =
pkgs.callPackage nix/tools/nixpkgsTools.nix { };
# Scripts for publishing new releases. # Scripts for publishing new releases.
release = release =
pkgs.callPackage nix/tools/release.nix { }; pkgs.callPackage nix/tools/release.nix { };
+10 -8
View File
@@ -19,26 +19,26 @@ write from scratch.
</a> </a>
</td> </td>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank"> <a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.svg"> <img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/neon.jpg">
</a> </a>
</td> </td>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://www.euronodes.com/postgrest" target="_blank"> <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/euronodes.svg"> <img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/code-build.png">
</a> </a>
</td> </td>
</tr> </tr>
<tr></tr> <tr></tr>
<tr> <tr>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank"> <a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/neon.jpg"> <img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/tembo.png">
</a> </a>
</td> </td>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank"> <a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/bytebase.svg"> <img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.png">
</a> </a>
</td> </td>
</tr> </tr>
@@ -61,3 +61,5 @@ The image is built from scratch using
no commands are listed in the image history. See the [PostgREST no commands are listed in the image history. See the [PostgREST
repository](https://github.com/PostgREST/postgrest/tree/main/nix/tools/docker) for repository](https://github.com/PostgREST/postgrest/tree/main/nix/tools/docker) for
details on the build process and how to inspect the image. details on the build process and how to inspect the image.
This does not apply to the arm64 variant, which is based on Ubuntu.
+3 -8
View File
@@ -40,14 +40,9 @@ database "PostgreSQL" {
:user: :user:
hexagon Proxy hexagon Proxy
:user: .r-> Proxy : request with JWT :user: .r-> Proxy
HTTPAPI <.l- Proxy HTTPAPI <.l- Proxy
hexagon ExternalAuth
ExternalAuth -u[hidden]- Proxy
:user: .r-> ExternalAuth : login
:user: <.r- ExternalAuth : JWT
:operator: .d-> HTTPADMIN :operator: .d-> HTTPADMIN
:operator: .d-> CLI :operator: .d-> CLI
@@ -56,8 +51,9 @@ PostgreSQL <.developer : "\t"
Listener -r.> "PostgreSQL" Listener -r.> "PostgreSQL"
"Connection Pool" -r.> "PostgreSQL" : "\t\t" "Connection Pool" -r.> "PostgreSQL" : "\t\t"
note bottom of Auth note bottom of Auth
Validates the JWT Authenticates the user request
end note end note
note bottom of ApiRequest note bottom of ApiRequest
@@ -76,7 +72,6 @@ note top of Listener
LISTEN session LISTEN session
end note end note
url of ExternalAuth is [[../explanations/external_auth.html]]
url of Admin is [[../references/admin_server.html#admin-server]] url of Admin is [[../references/admin_server.html#admin-server]]
url of API is [[../explanations/schema_isolation.html]] url of API is [[../explanations/schema_isolation.html]]
url of Auth is [[../references/auth.html#authn]] url of Auth is [[../references/auth.html#authn]]
+1 -1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 26 KiB

+1 -1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 27 KiB

+4 -5
View File
@@ -12,6 +12,7 @@
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
import sys
import os import os
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
@@ -48,14 +49,14 @@ source_suffix = ".rst"
# The master toctree document. # The master toctree document.
master_doc = "index" master_doc = "index"
# This is overridden by readthedocs with the version tag anyway # This is overriden by readthedocs with the version tag anyway
version = "devel" version = "13.0"
# To avoid repetition in <title> we set this to an empty string. # To avoid repetition in <title> we set this to an empty string.
release = "" release = ""
# General information about the project. # General information about the project.
project = "PostgREST " + version project = "PostgREST " + version
author = "The PostgREST contributors" author = "Joe Nelson, Steve Chavez"
copyright = "2017, " + author copyright = "2017, " + author
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
@@ -299,10 +300,8 @@ linkcheck_ignore = [
# 403 only in CI / GitHub Actions # 403 only in CI / GitHub Actions
r"https://www.patreon.com/postgrest", r"https://www.patreon.com/postgrest",
r"https://blog.frankel.ch/poor-man-api", r"https://blog.frankel.ch/poor-man-api",
r"https://www.cybertec-postgresql.com/.*",
# Odd SSL error # Odd SSL error
r"https://www.dripdepot.com", r"https://www.dripdepot.com",
r"https://www.euronodes.com",
# New GitHub UI delays comment load, so anchor fails # New GitHub UI delays comment load, so anchor fails
r"https://github.com/.*#issuecomment", r"https://github.com/.*#issuecomment",
# Random 500 Internal Server Error # Random 500 Internal Server Error
+2 -3
View File
@@ -6,7 +6,7 @@ Community Tutorials
* `Building a Contacts List with PostgREST and Vue.js <https://www.youtube.com/watch?v=iHtsALtD5-U>`_ - * `Building a Contacts List with PostgREST and Vue.js <https://www.youtube.com/watch?v=iHtsALtD5-U>`_ -
In this video series, DigitalOcean shows how to build and deploy an Nginx + PostgREST(using a managed PostgreSQL database) + Vue.js webapp in an Ubuntu server droplet. In this video series, DigitalOcean shows how to build and deploy an Nginx + PostgREST(using a managed PostgreSQL database) + Vue.js webapp in an Ubuntu server droplet.
* `PostgREST + Auth0: Create REST API in minutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest-1-introduction/>`_ - 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.
* `"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).
@@ -34,10 +34,9 @@ Templates
Example Apps Example Apps
------------ ------------
* `archtika <https://github.com/thiloho/archtika>`_ - self-hosted CMS * `archtika <https://github.com/thiloho/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
* `fullstack template <https://github.com/jenstroeger/fullstack-webapp-template>`_ - a complete fullstack webapp template using PG as db and message queue, Python and Dramatiq to implement async jobs, db migrations, test runners, and more.
* `general <https://github.com/PierreRochard/general>`_ - example auth back-end * `general <https://github.com/PierreRochard/general>`_ - example auth back-end
* `guild-operators <https://github.com/cardano-community/koios-artifacts/tree/main/files/grest>`_ - example queries and functions that the Cardano Community uses for their Guild Operators' Repository * `guild-operators <https://github.com/cardano-community/koios-artifacts/tree/main/files/grest>`_ - example queries and functions that the Cardano Community uses for their Guild Operators' Repository
* `PostGUI <https://github.com/priyank-purohit/PostGUI>`_ - React Material UI admin panel * `PostGUI <https://github.com/priyank-purohit/PostGUI>`_ - React Material UI admin panel
+2 -2
View File
@@ -13,7 +13,7 @@ A role can be thought of as either a database user, or a group of database users
Roles for Each Web User Roles for Each Web User
----------------------- -----------------------
PostgREST can accommodate either viewpoint. If you treat a role as a single user then :ref:`user_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.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. 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.
@@ -163,7 +163,7 @@ Another option is to define the function with the :code:`SECURITY DEFINER` optio
.. code-block:: postgres .. code-block:: postgres
-- login as a user which has privileges on the private schemas -- login as a user wich has privileges on the private schemas
-- create a sample function -- create a sample function
create or replace function login(email text, pass text, out token text) as $$ create or replace function login(email text, pass text, out token text) as $$
+1 -23
View File
@@ -16,7 +16,7 @@ Supported PostgreSQL versions
============================= =============================
=============== ================================= =============== =================================
**Supported** PostgreSQL >= 14 **Supported** PostgreSQL >= 12
=============== ================================= =============== =================================
PostgREST works with all PostgreSQL versions still `officially supported <https://www.postgresql.org/support/versioning/>`_. PostgREST works with all PostgreSQL versions still `officially supported <https://www.postgresql.org/support/versioning/>`_.
@@ -75,12 +75,8 @@ You can get the `official PostgREST Docker image <https://hub.docker.com/r/postg
.. code-block:: bash .. code-block:: bash
# pull the latest version
docker pull postgrest/postgrest docker pull postgrest/postgrest
# to pull a particular version, use one of the versions on https://hub.docker.com/r/postgrest/postgrest/tags
docker pull postgrest/postgrest:<version>
To configure the container image, use :ref:`env_variables_config`. To configure the container image, use :ref:`env_variables_config`.
There are two ways to run the PostgREST container: with an existing external database, or through docker-compose. There are two ways to run the PostgREST container: with an existing external database, or through docker-compose.
@@ -146,7 +142,6 @@ To avoid having to install the database at all, you can run both it and the serv
ports: ports:
- "3000:3000" - "3000:3000"
environment: environment:
PGRST_SERVER_HOST: 0.0.0.0 # necessary for `postgrest --ready` flag to work
PGRST_DB_URI: postgres://app_user:password@db:5432/app_db PGRST_DB_URI: postgres://app_user:password@db:5432/app_db
PGRST_OPENAPI_SERVER_PROXY_URI: http://127.0.0.1:3000 PGRST_OPENAPI_SERVER_PROXY_URI: http://127.0.0.1:3000
depends_on: depends_on:
@@ -181,23 +176,6 @@ If you want to have a visual overview of your API in your browser you can add sw
With this you can see the swagger-ui in your browser on port 8080. With this you can see the swagger-ui in your browser on port 8080.
.. _docker_cpu_contraint:
Docker Resource Constraints
---------------------------
PostgREST does not support ``--cpus`` `constraint option <https://docs.docker.com/engine/containers/resource_constraints/#configure-the-default-cfs-scheduler>`_.
As a workaround, you may use the `GHC RTS <https://ghc.gitlab.haskell.org/ghc/doc/users_guide/runtime_control.html#runtime-system-rts-options>`_ ``-N`` option. For instance, to limit it to 2 CPU cores, do:
.. code::
# Set environment variable GHCRTS set to "-N2"
docker run --rm -p 3000:3000 \
-e PGRST_DB_URI="postgres://app_user:password@10.0.0.10/postgres" \
-e GHCRTS="-N2"
postgrest/postgrest
.. _build_source: .. _build_source:
Building from Source Building from Source
@@ -43,7 +43,7 @@ As in :ref:`sql_user_management`, we create the :code:`pgcrypto` and :code:`pgjw
CREATE EXTENSION pgcrypto WITH SCHEMA ext_pgcrypto; CREATE EXTENSION pgcrypto WITH SCHEMA ext_pgcrypto;
Concerning the `pgjwt extension <https://github.com/michelp/pgjwt>`_, please cf. to :ref:`jwt-from-sql`. Concerning the `pgjwt extension <https://github.com/michelp/pgjwt>`_, please cf. to :ref:`client_auth`.
.. code-block:: postgres .. code-block:: postgres
@@ -157,7 +157,7 @@ Here we use the username instead of the email address to identify a user.
Logins Logins
~~~~~~ ~~~~~~
As described in :ref:`jwt-from-sql`, we'll create a JWT token inside our login function. Note that you'll need to adjust the secret key which is hard-coded in this example to a secure (at least thirty-two character) secret of your choosing. As described in :ref:`client_auth`, we'll create a JWT token inside our login function. Note that you'll need to adjust the secret key which is hard-coded in this example to a secure (at least thirty-two character) secret of your choosing.
.. code-block:: postgres .. code-block:: postgres
+1 -3
View File
@@ -3,7 +3,7 @@
SQL User Management SQL User Management
=================== ===================
As mentioned on :ref:`jwt_generation`, an external service can provide user management and coordinate with the PostgREST server using JWT. It's also possible to support logins entirely through SQL. It's a fair bit of work, so get ready. As mentioned on :ref:`jwt_generation`, an external service can provide user management and coordinate with the PostgREST server using JWT. Its also possible to support logins entirely through SQL. Its a fair bit of work, so get ready.
Storing Users and Passwords Storing Users and Passwords
--------------------------- ---------------------------
@@ -110,8 +110,6 @@ Then, add ``db-anon-role`` to the configuration file to allow anonymous requests
db-anon-role = "anon" db-anon-role = "anon"
.. _jwt-from-sql:
JWT from SQL JWT from SQL
~~~~~~~~~~~~ ~~~~~~~~~~~~
+31 -58
View File
@@ -38,53 +38,49 @@ Sponsors
.. image:: ../static/cybertec.svg .. image:: ../static/cybertec.svg
: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
.. container:: img-dark
.. image:: ../static/supabase-dark.svg
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
.. container:: img-light
.. image:: ../static/supabase.svg
: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
|
.. container:: img-dark .. container:: img-dark
.. image:: ../static/neon-dark.jpg .. image:: ../static/neon-dark.jpg
:target: https://neon.com/?utm_source=sponsor&utm_campaign=postgrest :target: https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest
.. container:: img-light .. container:: img-light
.. image:: ../static/neon.jpg .. image:: ../static/neon.jpg
:target: https://neon.com/?utm_source=sponsor&utm_campaign=postgrest :target: https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest
.. container:: img-dark .. container:: img-dark
.. image:: ../static/bytebase-dark.svg .. image:: ../static/code-build-dark.png
:target: https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest :target: https://code.build/?utm_source=sponsor&utm_campaign=postgrest
.. container:: img-light .. container:: img-light
.. image:: ../static/bytebase.svg .. image:: ../static/code-build.png
:target: https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest :target: https://code.build/?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/supabase-dark.png
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
.. container:: img-light
.. 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
.. image:: _static/empty.png
:target: #sponsors
.. The static/empty.png(created with `convert -size 320x95 xc:#fcfcfc empty.png`) is an ugly workaround .. The static/empty.png(created with `convert -size 320x95 xc:#fcfcfc empty.png`) is an ugly workaround
to create space and center the logos. It's not easy to layout with restructuredText. to create space and center the logos. It's not easy to layout with restructuredText.
.. image:: _static/empty.png .. .. image:: _static/empty.png
:target: #sponsors :target: #sponsors
| |
@@ -113,17 +109,10 @@ Getting Support
The project has a friendly and growing community. For discussions, use the Github `discussions page <https://github.com/PostgREST/postgrest/discussions>`_. You can also report or search for bugs/features on the Github `issues <https://github.com/PostgREST/postgrest/issues>`_ page. The project has a friendly and growing community. For discussions, use the Github `discussions page <https://github.com/PostgREST/postgrest/discussions>`_. You can also report or search for bugs/features on the Github `issues <https://github.com/PostgREST/postgrest/issues>`_ page.
Releases Release Notes
-------- -------------
PostgREST follows ``MAJOR.PATCH`` two-part versioning: The release notes are published on `PostgREST's GitHub release page <https://github.com/PostgREST/postgrest/releases>`_.
- ``MAJOR``: feature release, may deprecate or remove things.
- ``PATCH``: fix/security release only; no features, no behavior changes.
Starting from ``v14.0``, only even-numbered MAJOR versions will be released, reserving odd-numbered MAJOR versions for development.
All the releases are published on `PostgREST's GitHub release page <https://github.com/PostgREST/postgrest/releases>`_.
Tutorials Tutorials
--------- ---------
@@ -220,6 +209,7 @@ In Production
Here are some companies that use PostgREST in production. Here are some companies that use PostgREST in production.
* `Catarse <https://www.catarse.me>`_ * `Catarse <https://www.catarse.me>`_
* `Datrium <https://www.datrium.com>`_
* `Drip Depot <https://www.dripdepot.com>`_ * `Drip Depot <https://www.dripdepot.com>`_
* `Image-charts <https://www.image-charts.com>`_ * `Image-charts <https://www.image-charts.com>`_
* `Netwo <https://www.netwo.io>`_ * `Netwo <https://www.netwo.io>`_
@@ -271,21 +261,4 @@ Testimonials
Contributing Contributing
------------ ------------
Please see the `Contributing guidelines <https://github.com/PostgREST/postgrest/blob/main/CONTRIBUTING.md>`_ in the main PostgREST repository. Please see the `Contributing guidelines <https://github.com/PostgREST/postgrest/blob/main/.github/CONTRIBUTING.md>`_ in the main PostgREST repository.
.. raw:: html
<script type="text/javascript">
let hash = window.location.hash;
const redirects = {
// Tables and Views
'#release-notes': '#releases',
};
let willRedirectTo = redirects[hash];
if (willRedirectTo) {
window.location.href = willRedirectTo;
}
</script>
-8
View File
@@ -4,7 +4,6 @@ API's
APIs APIs
APISIX APISIX
AST AST
async
aud aud
Auth Auth
auth auth
@@ -15,7 +14,6 @@ BOM
Bytea Bytea
Cardano Cardano
cd cd
CDNs
centric centric
CLI CLI
CMS CMS
@@ -32,7 +30,6 @@ DDL
DOM DOM
DSL DSL
DevOps DevOps
Dramatiq
dockerize dockerize
enum enum
Enums Enums
@@ -44,7 +41,6 @@ EveryLayout
filename filename
FreeBSD FreeBSD
fts fts
fullstack
GeoJSON GeoJSON
Github Github
Google Google
@@ -178,7 +174,6 @@ unikernel
unix unix
updatable updatable
unfulfillable unfulfillable
unselected
Untyped Untyped
UPSERT UPSERT
Upsert Upsert
@@ -192,12 +187,9 @@ verifier
versioning versioning
Vondra Vondra
Vue Vue
webapp
webhooks webhooks
websearch websearch
Websockets Websockets
webuser webuser
wfts wfts
www www
debouncing
deduplicates
+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. 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. Both these endpoints reply with a status code and empty response body. Two endpoints ``live`` and ``ready`` will then be available.
.. important:: .. important::
-1
View File
@@ -21,7 +21,6 @@ PostgREST exposes three database objects of a schema as resources: tables, views
api/aggregate_functions.rst api/aggregate_functions.rst
api/openapi.rst api/openapi.rst
api/preferences.rst api/preferences.rst
api/vary_header.rst
api/* api/*
.. raw:: html .. raw:: html
@@ -173,4 +173,4 @@ Domain Representations avoid all the above drawbacks. Their only drawback is tha
Why not create a `base type <https://www.postgresql.org/docs/current/sql-createtype.html#id-1.9.3.94.5.8>`_ instead? ``CREATE TYPE app_uuid (INTERNALLENGTH = 22, INPUT = app_uuid_parser, OUTPUT = app_uuid_formatter)``. Why not create a `base type <https://www.postgresql.org/docs/current/sql-createtype.html#id-1.9.3.94.5.8>`_ instead? ``CREATE TYPE app_uuid (INTERNALLENGTH = 22, INPUT = app_uuid_parser, OUTPUT = app_uuid_formatter)``.
Creating base types need superuser, which is restricted on cloud hosted databases. Additionally this way lets "how the data is presented" dictate "how the data is stored" which would be backwards. Creating base types need superuser, which is restricted on cloud hosted databases. Additionally this way lets how the data is presented dictate how the data is stored which would be backwards.
-37
View File
@@ -69,26 +69,6 @@ If the function doesn't modify the database, it will also run under the GET meth
The function parameter names match the JSON object keys in the POST case, for the GET case they match the query parameters ``?a=1&b=2``. The function parameter names match the JSON object keys in the POST case, for the GET case they match the query parameters ``?a=1&b=2``.
If the function is defined to have default values for the parameters then arguments for these parameters can be omitted in the request. For instance:
.. code-block:: postgres
CREATE FUNCTION greet_user(username TEXT DEFAULT 'guest')
RETURNS TEXT AS $$
SELECT 'Hello ' || username || '!';
$$ LANGUAGE SQL IMMUTABLE;
.. code-block:: bash
curl -i "http://localhost:3000/rpc/greet_user"
.. code-block:: http
HTTP/1.1 200 OK
Context-Type: application/json; charset=utf-8
"Hello guest!"
.. _function_single_json: .. _function_single_json:
Functions with an array of JSON objects Functions with an array of JSON objects
@@ -314,23 +294,6 @@ Let's get its :ref:`explain_plan` when calling it with filters applied:
Notice there's no "Function Scan" node in the plan, which tells us it has been inlined. Notice there's no "Function Scan" node in the plan, which tells us it has been inlined.
Horizontal Filtering
~~~~~~~~~~~~~~~~~~~~
Table-valued functions support horizontal filtering on selected and unselected columns.
For example, the following RPC with filter on unselected column returns:
.. code-block:: bash
curl "http://localhost:3000/rpc/getallprojects?select=id,client_id&name=like.OSX"
.. code-block:: json
[
{ "id": 4, "client_id": 2 }
]
.. _scalar_functions: .. _scalar_functions:
Scalar functions Scalar functions
+1 -1
View File
@@ -15,7 +15,7 @@ Using these domains, :ref:`functions <functions>` can become handlers and `user-
.. important:: .. important::
- PostgREST vendor media types (``application/vnd.pgrst.plan``, ``application/vnd.pgrst.object`` and ``application/vnd.pgrst.array``) cannot be overridden. - PostgREST vendor media types (``application/vnd.pgrst.plan``, ``application/vnd.pgrst.object`` and ``application/vnd.pgrst.array``) cannot be overriden.
- Long media types like ``application/vnd.openxmlformats-officedocument.wordprocessingml.document`` cannot be expressed as domains since they surpass `PostgreSQL identifier length <https://www.postgresql.org/docs/current/limits.html#LIMITS-TABLE>`_. - Long media types like ``application/vnd.openxmlformats-officedocument.wordprocessingml.document`` cannot be expressed as domains since they surpass `PostgreSQL identifier length <https://www.postgresql.org/docs/current/limits.html#LIMITS-TABLE>`_.
For these you can use the :ref:`any_handler`. For these you can use the :ref:`any_handler`.
-8
View File
@@ -117,10 +117,6 @@ However, with ``handling=strict``, an invalid time zone preference will throw an
HTTP/1.1 400 Bad Request HTTP/1.1 400 Bad Request
.. note::
This feature requires querying `pg_timezone_names <https://www.postgresql.org/docs/current/view-pg-timezone-names.html>`_ during :ref:`schema_cache` load. If this is not desired, you can disable the feature with :ref:`db-timezone-enabled`.
.. _prefer_return: .. _prefer_return:
Return Representation Return Representation
@@ -296,7 +292,3 @@ With :ref:`RPC <functions>`, the preference is honored completely on the basis o
"details": "The query affects 14 rows", "details": "The query affects 14 rows",
"hint": null "hint": null
} }
.. note::
It is important for functions to return ``SETOF`` or ``TABLE`` when called with ``max-affected`` preference. A violation of this would cause a :ref:`PGRST128 <pgrst128>` error.
+10 -14
View File
@@ -143,7 +143,7 @@ Since the table name is plural, we can be more accurate by making it singular wi
One-to-many relationships One-to-many relationships
------------------------- -------------------------
The **foreign key reference** establishes the inverse one-to-many relationship. In this case, ``films`` returns as a JSON array because of the "to-many" end. The **foreign key reference** establishes the inverse one-to-many relationship. In this case, ``films`` returns as a JSON array because of the to-many end.
.. code-block:: bash .. code-block:: bash
@@ -251,12 +251,6 @@ Computed Relationships
You can manually define relationships by using functions. This is useful for database objects that can't define foreign keys, like `Foreign Data Wrappers <https://wiki.postgresql.org/wiki/Foreign_data_wrappers>`_. You can manually define relationships by using functions. This is useful for database objects that can't define foreign keys, like `Foreign Data Wrappers <https://wiki.postgresql.org/wiki/Foreign_data_wrappers>`_.
Computed relationships have good performance as their intended design enable `function inlining <https://wiki.postgresql.org/wiki/Inlining_of_SQL_functions#Inlining_conditions_for_table_functions>`_.
.. important::
- Always use ``SETOF`` when creating computed relationships. Functions can return a table without using ``SETOF``, but bear in mind that PostgreSQL will not inline them. e.g. ``RETURNS <table_name>`` is not inlinable.
Assuming there's a foreign table ``premieres`` that we want to relate to ``films``. Assuming there's a foreign table ``premieres`` that we want to relate to ``films``.
.. code-block:: postgres .. code-block:: postgres
@@ -289,10 +283,6 @@ The name of the function ``film`` is arbitrary and can be used to do the embeddi
".." ".."
] ]
.. warning::
- Make sure to correctly label the ``to-one`` part of the relationship. When using the ``ROWS 1`` estimation, PostgREST will expect a single row to be returned. If that is not the case, it will unnest the embedding and return repeated values for the top level resource.
Now let's define the opposite one-to-many relationship. Now let's define the opposite one-to-many relationship.
.. code-block:: postgres .. code-block:: postgres
@@ -341,6 +331,12 @@ Thanks to overloaded functions, you can use the same function name for different
Computed relationships have good performance as their intended design enable `function inlining <https://wiki.postgresql.org/wiki/Inlining_of_SQL_functions#Inlining_conditions_for_table_functions>`_. Computed relationships have good performance as their intended design enable `function inlining <https://wiki.postgresql.org/wiki/Inlining_of_SQL_functions#Inlining_conditions_for_table_functions>`_.
.. warning::
- Always use ``SETOF`` when creating computed relationships. Functions can return a table without using ``SETOF``, but bear in mind that PostgreSQL will not inline them.
- Make sure to correctly label the ``to-one`` part of the relationship. When using the ``ROWS 1`` estimation, PostgREST will expect a single row to be returned. If that is not the case, it will unnest the embedding and return repeated values for the top level resource.
.. _embed_disamb: .. _embed_disamb:
.. _target_disamb: .. _target_disamb:
.. _hint_disamb: .. _hint_disamb:
@@ -1210,7 +1206,7 @@ Spread on resources forming :ref:`one-to-many <one-to-many>` and :ref:`many-to-m
} }
] ]
Note that ``films`` is no longer an array of objects, unlike regularly embedding :ref:`one-to-many`. The embedded columns become arrays and they're correlated-in the above result, we can say that "Pulp Fiction" premiered in 1994 and "Reservoir Dogs" in 1992. Note that ``films`` is no longer an array of objects, unlike regularly embedding :ref:`one-to-many`. The embedded columns become arrays and they're correlatedin the above result, we can say that "Pulp Fiction" premiered in 1994 and "Reservoir Dogs" in 1992.
Order in spread to-many Order in spread to-many
~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~
@@ -1244,7 +1240,7 @@ You can order the correlated arrays explicitly. For example, to order by the fil
.. warning:: .. warning::
Aliasing spread columns is recommended since JSON allows duplicate keys. Example: Aliasing spreaded columns is recommended since JSON allows duplicate keys. Example:
.. code-block:: bash .. code-block:: bash
@@ -1306,7 +1302,7 @@ You can use multiple spreads at any level. For example, let's spread ``technical
Note that: Note that:
- All the ``film_*`` arrays are correlated-"Reservoir Dogs" premiered in 1992, its runtime is 1:39:00 and it has the following characters: ``[ "Mr. Pink", "Mr. White" ]``. - All the ``film_*`` arrays are correlated"Reservoir Dogs" premiered in 1992, its runtime is 1:39:00 and it has the following characters: ``[ "Mr. Pink", "Mr. White" ]``.
- The ``film_*`` arrays are ordered by ``year`` (due to ``films.order=year``). - The ``film_*`` arrays are ordered by ``year`` (due to ``films.order=year``).
- The bottom level array ``film_characters`` is ordered (due to ``films.roles.order=character``). - The bottom level array ``film_characters`` is ordered (due to ``films.roles.order=character``).
-4
View File
@@ -5,10 +5,6 @@ Schemas
PostgREST can expose a single or multiple schema's tables, views and functions. The :ref:`active database role <roles>` must have the usage privilege on the schemas to access them. PostgREST can expose a single or multiple schema's tables, views and functions. The :ref:`active database role <roles>` must have the usage privilege on the schemas to access them.
.. important::
``pg_catalog`` and ``information_schema`` are not allowed in :ref:`db-schemas`. This is done to prevent leaking sensitive information and hence they cannot be accessed directly. If you wish to expose objects of these schemas, expose another schema that contains wrapper views or functions over ``pg_catalog`` or ``information_schema`` objects.
Single schema Single schema
------------- -------------
+1 -10
View File
@@ -217,15 +217,6 @@ This allows using the ``fts`` operator on ``text`` and ``json`` types out of the
curl --get "http://localhost:3000/people" \ curl --get "http://localhost:3000/people" \
-d "my_json_column=not.phfts(english).The%20Fat%20Cats" -d "my_json_column=not.phfts(english).The%20Fat%20Cats"
.. important::
To ensure this operation is fast, you need to create an index on the expression:
.. code-block:: postgres
CREATE INDEX idx_people_col ON people
USING GIN (to_tsvector('french', my_text_column));
.. _v_filter: .. _v_filter:
Vertical Filtering Vertical Filtering
@@ -639,7 +630,7 @@ However, it can work with surrogate primary keys (e.g. ``id serial primary key``
.. code-block:: bash .. code-block:: bash
curl "http://localhost:3000/employees?columns=id,name,salary" \ curl "http://localhost:3000/employees?colums=id,name,salary" \
-X POST -H "Content-Type: application/json" \ -X POST -H "Content-Type: application/json" \
-H "Prefer: resolution=merge-duplicates, missing=default" \ -H "Prefer: resolution=merge-duplicates, missing=default" \
-d @- << EOF -d @- << EOF
+2 -2
View File
@@ -14,7 +14,7 @@ Custom Queries
The PostgREST URL grammar limits the kinds of queries clients can perform. It prevents arbitrary, potentially poorly constructed and slow client queries. It's good for quality of service, but means database administrators must create custom views and functions to provide richer endpoints. The most common causes for custom endpoints are The PostgREST URL grammar limits the kinds of queries clients can perform. It prevents arbitrary, potentially poorly constructed and slow client queries. It's good for quality of service, but means database administrators must create custom views and functions to provide richer endpoints. The most common causes for custom endpoints are
* SET operators like `UNION, INTERSECT and EXCEPT <https://www.postgresql.org/docs/current/queries-union.html>`_. * Table unions
* More complicated joins than those provided by :ref:`resource_embedding`. * More complicated joins than those provided by :ref:`resource_embedding`.
* Geo-spatial queries that require an argument, like "points near (lat,lon)" * Geo-spatial queries that require an argument, like "points near (lat,lon)"
@@ -51,7 +51,7 @@ You can request table/columns with spaces in them by percent encoding the spaces
Reserved characters Reserved characters
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
If filters include PostgREST reserved characters(``,``, ``.``, ``:``, ``*``, ``(``, ``)``) you'll have to surround them in percent encoded double quotes ``%22`` for correct processing. If filters include PostgREST reserved characters(``,``, ``.``, ``:``, ``()``) you'll have to surround them in percent encoded double quotes ``%22`` for correct processing.
Here ``Hebdon,John`` and ``Williams,Mary`` are values. Here ``Hebdon,John`` and ``Williams,Mary`` are values.
-16
View File
@@ -1,16 +0,0 @@
.. _vary_header:
Vary Header
===========
In order to assist caching proxies and CDNs, PostgREST includes a ``Vary`` header of value
``Accept, Prefer, Range`` in its responses which should fit most of the bills. As any other
response header, it's available for override
by updating ``response.headers`` GUC variable accordingly, for example:
.. code-block:: postgres
-- Override the Vary header to include Accept, Prefer and X-Test-Vary headers
perform set_config('response.headers', '[{"Vary": "Accept, Prefer, X-Test-Vary"}]', true);
In this case PostgREST will use provided value verbatim.
+42 -96
View File
@@ -31,7 +31,7 @@ The authenticator role is used for connecting to the database and should be conf
.. _user_impersonation: .. _user_impersonation:
User Impersonation User Impersonation
~~~~~~~~~~~~~~~~~~ ------------------
The picture below shows how the server handles authentication. If auth succeeds, it switches into the user role specified by the request, otherwise it switches into the anonymous role (if it's set in :ref:`db-anon-role`). The picture below shows how the server handles authentication. If auth succeeds, it switches into the user role specified by the request, otherwise it switches into the anonymous role (if it's set in :ref:`db-anon-role`).
@@ -43,13 +43,12 @@ This role switching mechanism is called **user impersonation**. In PostgreSQL it
The impersonated roles will have their settings applied. See :ref:`impersonated_settings`. The impersonated roles will have their settings applied. See :ref:`impersonated_settings`.
.. _jwt_auth: .. _jwt_impersonation:
JWT Authentication JWT-Based User Impersonation
------------------ ----------------------------
We use `JSON Web Tokens <https://datatracker.ietf.org/doc/html/rfc7519/>`_ to authenticate API requests, this allows us to be stateless and not require database lookups for verification. We use `JSON Web Tokens <https://jwt.io/>`_ to authenticate API requests, this allows us to be stateless and not require database lookups for verification. As you'll recall a JWT contains a list of cryptographically signed claims. All claims are allowed but PostgREST cares specifically about a claim called role.
As you'll recall a JWT contains a list of cryptographically signed claims. All claims are allowed but PostgREST cares specifically about a claim called role (configurable with :ref:`jwt_role_extract`).
.. code:: json .. code:: json
@@ -73,10 +72,17 @@ Note that the database administrator must allow the authenticator role to switch
If the client included no JWT (or one without a role claim) then PostgREST switches into the anonymous role. The database administrator must set the anonymous role permissions correctly to prevent anonymous users from seeing or changing things they shouldn't. If the client included no JWT (or one without a role claim) then PostgREST switches into the anonymous role. The database administrator must set the anonymous role permissions correctly to prevent anonymous users from seeing or changing things they shouldn't.
.. _bearer_auth: .. _jwt_generation:
Bearer Authentication JWT Generation
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
You can create a valid JWT either from inside your database (see :ref:`sql_user_management`) or via an external service (see :ref:`external_auth`).
.. _client_auth:
Client Auth
~~~~~~~~~~~
To make an authenticated request the client must include an :code:`Authorization` HTTP header with the value :code:`Bearer <jwt>`. For instance: To make an authenticated request the client must include an :code:`Authorization` HTTP header with the value :code:`Bearer <jwt>`. For instance:
@@ -87,29 +93,24 @@ To make an authenticated request the client must include an :code:`Authorization
The ``Bearer`` header value can be used with or without capitalization(``bearer``). The ``Bearer`` header value can be used with or without capitalization(``bearer``).
.. _jwt_generation: .. _jwt_caching:
JWT Generation JWT Caching
~~~~~~~~~~~~~~ -----------
You can create a valid JWT either from inside your database (see :ref:`sql_user_management`) or via an external service (see :ref:`external_auth`). PostgREST validates ``JWTs`` on every request. We can cache ``JWTs`` to avoid this performance overhead.
.. _jwt_signature: To enable JWT caching, the config :code:`jwt-cache-max-lifetime` is to be set. It is the maximum number of seconds for which the cache stores the JWT validation results. The cache uses the :code:`exp` claim to set the cache entry lifetime. If the JWT does not have an :code:`exp` claim, it uses the config value. See :ref:`jwt-cache-max-lifetime` for more details.
JWT Signature Verification .. note::
--------------------------
PostgREST supports both symmetric and asymmetric keys for verifying the signature of the token. You can use the :ref:`server-timing_header` to see the effect of JWT caching.
Symmetric Keys Symmetric Keys
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
In the case of symmetric cryptography the signer and verifier share the same secret passphrase, which can be configured with :ref:`jwt-secret`. Each token is cryptographically signed with a secret key. In the case of symmetric cryptography the signer and verifier share the same secret passphrase, which can be configured with :ref:`jwt-secret`.
If it is set to a simple string then PostgREST interprets it as an HMAC-SHA256 passphrase. If it is set to a simple string value like “reallyreallyreallyreallyverysafe” then PostgREST interprets it as an HMAC-SHA256 passphrase.
.. code-block:: ini
jwt-secret = "reallyreallyreallyreallyverysafe"
.. _asym_keys: .. _asym_keys:
@@ -155,39 +156,39 @@ You can specify the literal value as we saw earlier, or reference a filename to
jwt-secret = "@rsa.jwk.pub" jwt-secret = "@rsa.jwk.pub"
``kid`` verification JWK ``kid`` validation
^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^
PostgREST has built-in verification of the `key ID parameter <https://www.rfc-editor.org/rfc/rfc7517#section-4.5>`_, useful when working with a JSON Web Key Set. PostgREST has built-in validation of the `key ID parameter <https://www.rfc-editor.org/rfc/rfc7517#section-4.5>`_, useful when working with a JWK Set.
It goes as follows: It goes as follows:
- If the JWT contains a ``kid`` parameter, then PostgREST will look for the JSON Web Key in the :ref:`jwt-secret`. - If the JWT contains a ``kid`` parameter, then PostgREST will look for the JWK in the :ref:`jwt-secret`.
+ If no key has a matching ``kid`` (or if they don't have one defined), the token will be rejected with a :ref:`401 Unauthorized <pgrst301>` error. + If no JWK matches the same ``kid`` value (or if they do not have a ``kid``), then the token will be rejected with a :ref:`401 Unauthorized <pgrst301>` error.
+ If a key matches the ``kid`` value then it will validate the token against that key accordingly. + If a JWK matches the ``kid`` value then it will validate the token against that JWK accordingly.
- If the JWT doesn't have a ``kid``, PostgREST will try each key in the :ref:`jwt-secret` one by one until it finds one that works. - If the JWT does not have a ``kid`` parameter, then PostgREST will validate the token against each JWK in the :ref:`jwt-secret`.
.. _jwt_claims_validation: .. _jwt_claims_validation:
JWT Claims Validation JWT Claims Validation
--------------------- ~~~~~~~~~~~~~~~~~~~~~
Time-Based claims validation PostgREST honors the following `JWT claims <https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4>`_:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The time-based JWT claims specified in `RFC 7519 <https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4>`_ are validated:
- ``exp`` Expiration Time - ``exp`` Expiration Time
- ``iat`` Issued At - ``iat`` Issued At
- ``nbf`` Not Before - ``nbf`` Not Before
- ``aud`` :ref:`Audience <jwt_aud_validation>`
We allow a 30-second clock skew when validating the above claims. In other words, we give an extra 30 seconds before the JWT is rejected if there is a slight discrepancy in the timestamps. .. note::
PostgREST allows for a 30-second clock skew when validating the ``exp``, ``iat`` and ``nbf`` 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_aud: .. _jwt_aud_validation:
``aud`` validation JWT ``aud`` Claim Validation
~~~~~~~~~~~~~~~~~~ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
PostgREST has built-in validation of the `JWT audience claim <https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3>`_. PostgREST has built-in validation of the `JWT audience claim <https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3>`_.
It works this way: It works this way:
@@ -200,29 +201,10 @@ It works this way:
+ If the match fails or if the ``aud`` value is not a string or array of strings, then the token will be rejected with a :ref:`401 Unauthorized <pgrst303>` error. + If the match fails or if the ``aud`` value is not a string or array of strings, then the token will be rejected with a :ref:`401 Unauthorized <pgrst303>` error.
+ If the ``aud`` key **is not present** or if its value is ``null`` or ``[]``, PostgREST will interpret this token as allowed for all audiences and will complete the request. + If the ``aud`` key **is not present** or if its value is ``null`` or ``[]``, PostgREST will interpret this token as allowed for all audiences and will complete the request.
.. _jwt_caching:
JWT Cache
---------
JWT signature validation (specially :ref:`asym_keys` such as RSA) is slow, we can cache ``JWT`` validation results to avoid this performance overhead.
The JWT cache is bounded and uses the `SIEVE algorithm <https://cachemon.github.io/SIEVE-website>`_ for efficient eviction. The cache is enabled by default and can be configured with :ref:`jwt-cache-max-entries`.
It's recommended to leave the JWT cache enabled as our load tests indicate ~20% more throughput for simple GET requests when using it. This while reducing CPU utilization in exchange for a bit more memory.
:ref:`jwt_cache_metrics` are available.
.. note::
- If the ``jwt-secret`` is changed and the config is reloaded, the JWT cache will reset.
- JWTs that pass :ref:`jwt_signature` are cached, regardless if they pass :ref:`jwt_claims_validation`. We do this to ensure responses stays fast under common failure cases (such as expired JWTs).
- You can use the :ref:`server-timing_header` to see the performance benefit of JWT caching.
.. _jwt_role_extract: .. _jwt_role_extract:
JWT Role Extraction JWT Role Extraction
------------------- ~~~~~~~~~~~~~~~~~~~
A JSPath DSL that specifies the location of the :code:`role` key in the JWT claims. It's configured by :ref:`jwt-role-claim-key`. This can be used to consume a JWT provided by a third party service like Auth0, Okta, Microsoft Entra or Keycloak. A JSPath DSL that specifies the location of the :code:`role` key in the JWT claims. It's configured by :ref:`jwt-role-claim-key`. This can be used to consume a JWT provided by a third party service like Auth0, Okta, Microsoft Entra or Keycloak.
@@ -234,17 +216,6 @@ The DSL follows the `JSONPath <https://goessner.net/articles/JsonPath/>`_ expres
- ``==^`` selects the first array element that ends with the right operand - ``==^`` selects the first array element that ends with the right operand
- ``*==`` selects the first array element that contains the right operand - ``*==`` selects the first array element that contains the right operand
The selected role value can also be sliced using the slice operator ``[a:b]``. It is similar to `slice operator in python <https://docs.python.org/3/library/functions.html#slice>`_. Negative index values are also supported. The syntax is as:
- ``[a:b]`` take slice from index ``a`` up to ``b``
- ``[a:]`` take slice from index ``a`` to end
- ``[:b]`` take slice from start to index ``b``
- ``[:]`` select everything, no slicing
.. important::
Make sure that you are not taking a slice where the start index comes after the end index like ``[11:2]``. The result of this would be empty string and so no role would get selected.
Usage examples: Usage examples:
.. code:: bash .. code:: bash
@@ -266,17 +237,12 @@ Usage examples:
jwt-role-claim-key = ".postgrest.roles[?(@ ==^ \"hor\")]" jwt-role-claim-key = ".postgrest.roles[?(@ ==^ \"hor\")]"
jwt-role-claim-key = ".postgrest.roles[?(@ *== \"utho\")]" jwt-role-claim-key = ".postgrest.roles[?(@ *== \"utho\")]"
# {"postgrest":{"wlcg": ["/groupa", "/groupb/"]}}
# skip the "/" character using slice operator
jwt-role-claim-key = ".postgrest.wlcg[0][1:]"
jwt-role-claim-key = ".postgrest.wlcg[1][1:-1]"
.. note:: .. note::
The string comparison operators are implemented as a custom extension to the JSPath and does not strictly follow the `RFC 9535 <https://www.rfc-editor.org/rfc/rfc9535.html>`_. The string comparison operators are implemented as a custom extension to the JSPath and does not strictly follow the `RFC 9535 <https://www.rfc-editor.org/rfc/rfc9535.html>`_.
JWT Security JWT Security
------------ ~~~~~~~~~~~~
There are at least three types of common critiques against using JWT: 1) against the standard itself, 2) against using libraries with known security vulnerabilities, and 3) against using JWT for web sessions. We'll briefly explain each critique, how PostgREST deals with it, and give recommendations for appropriate user action. There are at least three types of common critiques against using JWT: 1) against the standard itself, 2) against using libraries with known security vulnerabilities, and 3) against using JWT for web sessions. We'll briefly explain each critique, how PostgREST deals with it, and give recommendations for appropriate user action.
@@ -312,23 +278,3 @@ doing custom logic based on the web user info.
END IF; END IF;
END END
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
.. raw:: html
<script type="text/javascript">
let hash = window.location.hash;
const redirects = {
'#jwt-based-user-impersonation': '#jwt-authentication',
'#client-auth': '#bearer-authentication',
'#jwt-caching': '#jwt-cache',
'#jwk-kid-validation': '#kid-verification',
'#jwt-aud-claim-validation': '#aud-validation',
};
let willRedirectTo = redirects[hash];
if (willRedirectTo) {
window.location.href = willRedirectTo;
}
</script>
+8 -46
View File
@@ -3,47 +3,23 @@
CLI CLI
=== ===
PostgREST provides a CLI with the options listed below: PostgREST provides a CLI with the commands listed below:
.. code:: text
Usage: postgrest [-v|--version] [-e|--example] [--dump-config | --dump-schema | --ready]
[FILENAME]
PostgREST / create a REST API to an existing Postgres
database
Available options:
-h,--help Show this help text
-v,--version Show the version information
-e,--example Show an example configuration file
--dump-config Dump loaded configuration and exit
--dump-schema Dump loaded schema as JSON and exit (for debugging,
output structure is unstable)
--ready Checks the health of PostgREST by doing a request on
the admin server /ready endpoint
FILENAME Path to configuration file
FILENAME
--------
Runs PostgREST with the given :ref:`file_config`.
Help Help
---- ----
.. code:: bash .. code:: bash
$ postgrest --help $ postgrest [-h|--help]
Shows all the options available. Shows all the commands available.
Version Version
------- -------
.. code:: bash .. code:: bash
$ postgrest --version $ postgrest [-v|--version]
Prints the PostgREST version. Prints the PostgREST version.
@@ -52,16 +28,16 @@ Example
.. code:: bash .. code:: bash
$ postgrest --example $ postgrest [-e|--example]
Shows example configuration settings. Shows example configuration options.
Dump Config Dump Config
----------- -----------
.. code:: bash .. code:: bash
$ postgrest --dump-config $ postgrest [--dump-config]
Dumps the loaded :ref:`configuration` values, considering the configuration file, environment variables and :ref:`in_db_config`. Dumps the loaded :ref:`configuration` values, considering the configuration file, environment variables and :ref:`in_db_config`.
@@ -70,20 +46,6 @@ Dump Schema
.. code:: bash .. code:: bash
$ postgrest --dump-schema $ postgrest [--dump-schema]
Dumps the schema cache in JSON format. Dumps the schema cache in JSON format.
Ready Flag
----------
Makes a request to the ``/ready`` endpoint of the :ref:`admin_server`. It exits with a return code of ``0`` on success and ``1`` on failure.
.. code-block:: bash
$ postgrest --ready
OK: http://localhost:3001/ready
.. note::
The ``--ready`` flag cannot be used when :ref:`server-host` is configured with special hostnames. We suggest to change it to ``localhost``.
+24 -57
View File
@@ -195,33 +195,6 @@ app.settings.*
The :code:`current_setting` function has `an optional boolean second <https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADMIN-SET>`_ argument to avoid it from raising an error if the value was not defined. Default values to :code:`app.settings` can then be given by combining this argument with :code:`coalesce` and :code:`nullif` : :code:`coalesce(nullif(current_setting('app.settings.my_custom_variable', true), ''), 'default value')`. The use of :code:`nullif` is necessary because if set in a transaction, the setting is sometimes not "rolled back" to :code:`null`. See also :ref:`this section <guc_req_headers_cookies_claims>` for more information on this behaviour. The :code:`current_setting` function has `an optional boolean second <https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADMIN-SET>`_ argument to avoid it from raising an error if the value was not defined. Default values to :code:`app.settings` can then be given by combining this argument with :code:`coalesce` and :code:`nullif` : :code:`coalesce(nullif(current_setting('app.settings.my_custom_variable', true), ''), 'default value')`. The use of :code:`nullif` is necessary because if set in a transaction, the setting is sometimes not "rolled back" to :code:`null`. See also :ref:`this section <guc_req_headers_cookies_claims>` for more information on this behaviour.
.. _client-error-verbosity:
client-error-verbosity
----------------------
=============== =======================
**Type** String
**Default** verbose
**Reloadable** Y
**Environment** PGRST_CLIENT_ERROR_VERBOSITY
**In-Database** pgrst.client_error_verbosity
=============== =======================
Specifies the verbosity of PostgREST errors. See :ref:`client_error_verbosity`.
.. code:: bash
# Return error "code", "message", "details" and "hint"
client-error-verbosity = "verbose"
# Return only "code" and "message"
client-error-verbosity = "minimal"
.. note::
This setting only affects client side error messages. Server side logs are not affected by this setting.
.. _db-aggregates-enabled: .. _db-aggregates-enabled:
db-aggregates-enabled db-aggregates-enabled
@@ -291,7 +264,7 @@ db-channel-enabled
When this is set to :code:`true`, the notification channel specified in :ref:`db-channel` is enabled. When this is set to :code:`true`, the notification channel specified in :ref:`db-channel` is enabled.
You should set this to ``false`` when using PostgreSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section <external_connection_poolers>` for more information. You should set this to ``false`` when using PostgresSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section <external_connection_poolers>` for more information.
.. _db-config: .. _db-config:
@@ -343,7 +316,7 @@ db-extra-search-path
Multiple schemas can be added in a comma-separated string, e.g. ``public, extensions``. Multiple schemas can be added in a comma-separated string, e.g. ``public, extensions``.
.. important:: .. important::
We default this config to ``public`` because it is the most common schema used to install PostgreSQL extensions such as :ref:`PostGIS <ww_postgis>`. You can disable this by setting this config to ``""``. We default this config to ``public`` because it is the most common schema used to install PostgreSQL extensions such as :ref:`PostGIS <ww_postgis>`. You can disable this by setting this config to ``""``.
.. _db-hoisted-tx-settings: .. _db-hoisted-tx-settings:
@@ -436,7 +409,7 @@ db-pool-max-idletime
**In-Database** `n/a` **In-Database** `n/a`
=============== ================================= =============== =================================
*For backwards compatibility, this config parameter is also available as "db-pool-timeout".* *For backwards compatibility, this config parameter is also available as db-pool-timeout.*
Time in seconds to close idle pool connections. Time in seconds to close idle pool connections.
@@ -506,7 +479,7 @@ db-prepared-statements
When disabled, the generated queries will be parameterized (invulnerable to SQL injection) but they will not be prepared (cached in the database session). Not using prepared statements will noticeably decrease performance, so it's recommended to always have this setting enabled. When disabled, the generated queries will be parameterized (invulnerable to SQL injection) but they will not be prepared (cached in the database session). Not using prepared statements will noticeably decrease performance, so it's recommended to always have this setting enabled.
You should only set this to ``false`` when using PostgreSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section <external_connection_poolers>` for more information. You should only set this to ``false`` when using PostgresSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section <external_connection_poolers>` for more information.
.. _db-root-spec: .. _db-root-spec:
@@ -540,21 +513,6 @@ db-schemas
The list of database schemas to expose to clients. See :ref:`schemas`. The list of database schemas to expose to clients. See :ref:`schemas`.
.. _db-timezone-enabled:
db-timezone-enabled
-------------------
=============== =================================
**Type** Boolean
**Default** True
**Reloadable** Y
**Environment** PGRST_DB_TIMEZONE_ENABLED
**In-Database** pgrst.db_timezone_enabled
=============== =================================
Enables the use of :ref:`prefer_timezone` preference header. Disabled when set to ``false``.
.. _db-tx-end: .. _db-tx-end:
db-tx-end db-tx-end
@@ -568,7 +526,7 @@ db-tx-end
**In-Database** pgrst.db_tx_end **In-Database** pgrst.db_tx_end
=============== ================================= =============== =================================
Specifies how to terminate the database transactions. See :ref:`prefer_tx`. Specifies how to terminate the database transactions.
.. code:: bash .. code:: bash
@@ -645,7 +603,7 @@ jwt-aud
**In-Database** pgrst.jwt_aud **In-Database** pgrst.jwt_aud
=============== ================================= =============== =================================
Specifies an audience for the JWT ``aud`` claim. See :ref:`jwt_aud`. Specifies an audience for the JWT ``aud`` claim. See :ref:`jwt_aud_validation`.
.. _jwt-role-claim-key: .. _jwt-role-claim-key:
@@ -700,20 +658,20 @@ jwt-secret-is-base64
When this is set to :code:`true`, the value derived from :code:`jwt-secret` will be treated as a base64 encoded secret. When this is set to :code:`true`, the value derived from :code:`jwt-secret` will be treated as a base64 encoded secret.
.. _jwt-cache-max-entries: .. _jwt-cache-max-lifetime:
jwt-cache-max-entries jwt-cache-max-lifetime
---------------------- ----------------------
=============== ================================= =============== =================================
**Type** Int **Type** Int
**Default** 1000 **Default** 0
**Reloadable** Y **Reloadable** Y
**Environment** PGRST_JWT_CACHE_MAX_ENTRIES **Environment** PGRST_JWT_CACHE_MAX_LIFETIME
**In-Database** pgrst.jwt_cache_max_entries **In-Database** pgrst.jwt_cache_max_lifetime
=============== ================================= =============== =================================
Maximum number of entries in JWT cache. The value :code:`0` disables JWT caching. See :ref:`jwt_caching`. Maximum number of seconds of lifetime for cached entries. The default :code:`0` disables caching. See :ref:`jwt_caching`.
.. _log-level: .. _log-level:
@@ -756,14 +714,23 @@ log-query
--------- ---------
=============== ================================= =============== =================================
**Type** Boolean **Type** String
**Default** False **Default** "disabled"
**Reloadable** Y **Reloadable** Y
**Environment** PGRST_LOG_QUERY **Environment** PGRST_LOG_QUERY
**In-Database** `n/a` **In-Database** `n/a`
=============== ================================= =============== =================================
Logs the SQL query for the corresponding request at the current :ref:`log-level`. See :ref:`sql_query_logs`. Logs the SQL query for the corresponding request at the current :ref:`log-level`.
See :ref:`sql_query_logs`.
.. code:: bash
# Logs the main SQL query
log-query = "main-query"
# Disables logging the SQL query
log-query = "disabled"
.. _openapi-mode: .. _openapi-mode:
+2 -2
View File
@@ -15,7 +15,7 @@ Dynamic Connection Pool
To conserve system resources, PostgREST uses a dynamic connection pool. This enables the number of connections in the pool to increase and decrease depending on request traffic. To conserve system resources, PostgREST uses a dynamic connection pool. This enables the number of connections in the pool to increase and decrease depending on request traffic.
- If all the connections are being used, a new connection is added. The pool can grow until it reaches the :ref:`db-pool` size. Note that it's pointless to set this higher than the ``max_connections`` setting in your database. - If all the connections are being used, a new connection is added. The pool can grow until it reaches the :ref:`db-pool` size. Note that its pointless to set this higher than the ``max_connections`` setting in your database.
- If a connection is unused for a period of time (:ref:`db-pool-max-idletime`), it will be released. - If a connection is unused for a period of time (:ref:`db-pool-max-idletime`), it will be released.
- For connecting to the database, the :ref:`authenticator <roles>` role is used. You can configure this using :ref:`db-uri`. - For connecting to the database, the :ref:`authenticator <roles>` role is used. You can configure this using :ref:`db-uri`.
@@ -106,4 +106,4 @@ Also set :ref:`db-channel-enabled` to ``false`` since ``LISTEN`` is not compatib
.. note:: .. note::
It's not recommended to use an external connection pooler. `Our benchmarks <https://github.com/PostgREST/postgrest/issues/2294#issuecomment-1139148672>`_ indicate it provides much lower performance than PostgREST built-in pool. Its not recommended to use an external connection pooler. `Our benchmarks <https://github.com/PostgREST/postgrest/issues/2294#issuecomment-1139148672>`_ indicate it provides much lower performance than PostgREST built-in pool.
+1 -40
View File
@@ -267,10 +267,6 @@ Related to the HTTP request elements.
| | | implemented. | | | | implemented. |
| PGRST127 | | | | PGRST127 | | |
+---------------+-------------+-------------------------------------------------------------+ +---------------+-------------+-------------------------------------------------------------+
| .. _pgrst128: | 400 | ``max-affected`` preference is violated with ``RPC`` call. |
| | | See :ref:`prefer_max_affected`. |
| PGRST128 | | |
+---------------+-------------+-------------------------------------------------------------+
.. _pgrst2**: .. _pgrst2**:
@@ -329,7 +325,7 @@ Related to the authentication process using JWT. You can follow the :ref:`tut1`
| PGRST301 | | | | PGRST301 | | |
+---------------+-------------+-------------------------------------------------------------+ +---------------+-------------+-------------------------------------------------------------+
| .. _pgrst302: | 401 | Attempted to do a request without | | .. _pgrst302: | 401 | Attempted to do a request without |
| | | :ref:`bearer_auth` when the anonymous role | | | | :ref:`authentication <client_auth>` when the anonymous role |
| PGRST302 | | is disabled by not setting it in :ref:`db-anon-role`. | | PGRST302 | | is disabled by not setting it in :ref:`db-anon-role`. |
+---------------+-------------+-------------------------------------------------------------+ +---------------+-------------+-------------------------------------------------------------+
| .. _pgrst303: | 401 | :ref:`JWT claims validation <jwt_claims_validation>` | | .. _pgrst303: | 401 | :ref:`JWT claims validation <jwt_claims_validation>` |
@@ -473,38 +469,3 @@ For example, doing a request on a table with high count (say 30_000_000), we get
Proxy-Status: PostgREST; error=57014 Proxy-Status: PostgREST; error=57014
The PostgreSQL error code ``57014`` (`ref <https://www.postgresql.org/docs/current/errcodes-appendix.html>`_) reveals that the error is due to a short ``statement_timeout`` value. The PostgreSQL error code ``57014`` (`ref <https://www.postgresql.org/docs/current/errcodes-appendix.html>`_) reveals that the error is due to a short ``statement_timeout`` value.
.. _client_error_verbosity:
Client Error Verbosity
======================
For HTTP clients, the error verbosity can be set via :ref:`client-error-verbosity` config.
With ``verbose``, it returns ``code``, ``message``, ``details`` and ``hint``.
.. code:: bash
curl "localhost:3000/itemsxx"
.. code-block:: json
{
"code": "PGRST205",
"message": "Could not find the table 'public.itemsxx' in the schema cache",
"details": "Perhaps you meant the table 'public.items'",
"hint": null
}
With ``minimal``, just ``code`` and ``message`` is returned.
.. code:: bash
curl "localhost:3000/itemsxx"
.. code-block:: json
{
"code": "PGRST205",
"message": "Could not find the table 'public.itemsxx' in the schema cache"
}
+2 -4
View File
@@ -4,7 +4,7 @@ Listener
######## ########
PostgREST uses `LISTEN <https://www.postgresql.org/docs/current/sql-listen.html>`_ to reload its :ref:`Schema Cache <schema_reloading_notify>` and :ref:`Configuration <config_reloading_notify>` via `NOTIFY <https://www.postgresql.org/docs/current/sql-notify.html>`_. PostgREST uses `LISTEN <https://www.postgresql.org/docs/current/sql-listen.html>`_ to reload its :ref:`Schema Cache <schema_reloading_notify>` and :ref:`Configuration <config_reloading_notify>` via `NOTIFY <https://www.postgresql.org/docs/current/sql-notify.html>`_.
This is useful in environments where you can't send SIGUSR1 or SIGUSR2 Unix Signals. This is useful in environments where you cant send SIGUSR1 or SIGUSR2 Unix Signals.
Like on cloud managed containers or on Windows systems. Like on cloud managed containers or on Windows systems.
.. code:: postgresql .. code:: postgresql
@@ -46,9 +46,7 @@ This will cause the :ref:`connection_pool` to connect to the read replica host a
.. note:: .. note::
- Under the hood, PostgREST forces `target_session_attrs=read-write <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-TARGET-SESSION-ATTRS>`_ for the ``LISTEN`` session. Under the hood, PostgREST forces `target_session_attrs=read-write <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-TARGET-SESSION-ATTRS>`_ for the ``LISTEN`` session.
So if you specify ``target_session_attrs=read-only`` as mentioned above, PostgREST will override it for the ``LISTEN``.
- ``read-only`` is only available on libpq >= 14, if you use a lower version you will get an error like ``invalid target_session_attrs value: \"read-only\"``.
.. _listener_automatic_recovery: .. _listener_automatic_recovery:
+7 -44
View File
@@ -52,12 +52,14 @@ For diagnostic information about the server itself, PostgREST logs to ``stderr``
SQL Query Logs SQL Query Logs
-------------- --------------
To log the SQL queries executed for a request, set the :ref:`log-query` to ``true``. It will be logged based on the current :ref:`log-level` setting. To log the :ref:`main SQL query <main_query>` executed for a request, set the :ref:`log-query` to ``main-query``.
It will be logged based on the current :ref:`log-level` setting.
For example, with this configuration:
.. code-block:: bash .. code-block:: bash
log-level = "warn" log-level = "warn"
log-query = "true" log-query = "main-query"
The SQL queries will only be logged on ``400`` HTTP errors and up. The SQL queries will only be logged on ``400`` HTTP errors and up.
So, if the user requests a resource without sufficient privileges: So, if the user requests a resource without sufficient privileges:
@@ -120,17 +122,12 @@ Restart the database and watch the log file in real-time to understand how HTTP
Metrics Metrics
======= =======
The ``metrics`` endpoint on the :ref:`admin_server` endpoint provides metrics in `Prometheus text format <https://prometheus.io/docs/instrumenting/exposition_formats/#prometheus-text-format>`_. The ``metrics`` endpoint on the :ref:`admin_server` endpoint provides metrics in `Prometheus text format <https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format>`_.
.. code-block:: bash .. code-block:: bash
curl "http://localhost:3001/metrics" curl "http://localhost:3001/metrics"
.. code-block:: http
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
# HELP pgrst_schema_cache_query_time_seconds The query time in seconds of the last schema cache load # HELP pgrst_schema_cache_query_time_seconds The query time in seconds of the last schema cache load
# TYPE pgrst_schema_cache_query_time_seconds gauge # TYPE pgrst_schema_cache_query_time_seconds gauge
pgrst_schema_cache_query_time_seconds 1.5937927e-2 pgrst_schema_cache_query_time_seconds 1.5937927e-2
@@ -204,40 +201,6 @@ pgrst_db_pool_max
Max pool connections. Max pool connections.
.. _jwt_cache_metrics:
JWT Cache Metrics
-----------------
Metrics related to the :ref:`jwt_caching`.
pgrst_jwt_cache_requests_total
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
======== =======
**Type** Counter
======== =======
The total number of JWT cache lookups.
pgrst_jwt_cache_hits_total
~~~~~~~~~~~~~~~~~~~~~~~~~~
======== =======
**Type** Counter
======== =======
The total number of JWT cache hits.
pgrst_jwt_cache_evictions_total
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
======== =======
**Type** Counter
======== =======
The total number of JWT cache evictions.
Traces Traces
====== ======
@@ -283,7 +246,7 @@ See :ref:`proxy-status_header`.
Server-Timing Header Server-Timing Header
-------------------- --------------------
You can enable the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/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/Headers/Server-Timing>`_ header by setting :ref:`server-timing-enabled` on.
This header communicates metrics of the different phases in the request-response cycle. This header communicates metrics of the different phases in the request-response cycle.
.. code-block:: bash .. code-block:: bash
@@ -297,7 +260,7 @@ This header communicates metrics of the different phases in the request-response
Server-Timing: jwt;dur=14.9, parse;dur=71.1, plan;dur=109.0, transaction;dur=353.2, response;dur=4.4 Server-Timing: jwt;dur=14.9, parse;dur=71.1, plan;dur=109.0, transaction;dur=353.2, response;dur=4.4
- All the durations (``dur``) are in milliseconds. - All the durations (``dur``) are in milliseconds.
- The ``jwt`` stage is when :ref:`jwt_auth` is done. This duration can be lowered with :ref:`jwt_caching`. - The ``jwt`` stage is when :ref:`jwt_impersonation` is done. This duration can be lowered with :ref:`jwt_caching`.
- On the ``parse`` stage, the :ref:`url_grammar` is parsed. - On the ``parse`` stage, the :ref:`url_grammar` is parsed.
- On the ``plan`` stage, the :ref:`schema_cache` is used to generate the :ref:`main_query` of the transaction. - On the ``plan`` stage, the :ref:`schema_cache` is used to generate the :ref:`main_query` of the transaction.
- The ``transaction`` stage corresponds to the database transaction. See :ref:`transactions`. - The ``transaction`` stage corresponds to the database transaction. See :ref:`transactions`.
+1 -20
View File
@@ -3,16 +3,10 @@
Schema Cache Schema Cache
============ ============
PostgREST requires metadata from the database to provide a REST API that abstracts SQL details. One example of this is the interface for :ref:`resource_embedding`. PostgREST requires metadata from the database schema to provide a REST API that abstracts SQL details. One example of this is the interface for :ref:`resource_embedding`.
Getting this metadata requires expensive queries. To avoid repeating this work, PostgREST uses a schema cache. Getting this metadata requires expensive queries. To avoid repeating this work, PostgREST uses a schema cache.
.. note::
- Schema cache queries have been optimized over time to stay fast, even on complex databases. You can see a summary of their execution time in :ref:`pgrst_logging` and :ref:`metrics`.
- If the schema cache queries are slow, the most likely cause is *system catalog bloat*, see `issue#3212 <https://github.com/PostgREST/postgrest/issues/3212>`_ for more details.
- You can turn the :ref:`log-level` to ``debug`` to see the time of each schema cache query.
.. _schema_reloading: .. _schema_reloading:
Schema Cache Reloading Schema Cache Reloading
@@ -59,19 +53,6 @@ To reload the schema cache from within the database, you can use the ``NOTIFY``
NOTIFY pgrst, 'reload schema' NOTIFY pgrst, 'reload schema'
Debouncing
~~~~~~~~~~
PostgREST does not reload the schema cache for each notification when several ``NOTIFY pgrst`` events are generated quickly after one another.
There are two cases to consider: when notifications are sent within a single transaction and when they are sent across multiple transactions.
In the first case, PostgreSQL deduplicates identical ``NOTIFY`` events within the same transaction. This means that even if multiple ``NOTIFY pgrst`` statements are executed before a ``COMMIT``, only a single notification is delivered to PostgREST.
In the second case, when notifications are sent from separate transactions in a short time span, PostgREST applies a debouncing mechanism to avoid excessive schema cache reloads.
Instead of reloading the schema cache for each notification, events are grouped within a small time window of 100 milliseconds. The reload function is executed once immediately when the first notification is received and once more after the burst of events settles, resulting in at most two executions within that time window.
.. _auto_schema_reloading: .. _auto_schema_reloading:
Automatic Schema Cache Reloading Automatic Schema Cache Reloading
+1 -1
View File
@@ -221,7 +221,7 @@ Notice that the ``response.headers`` should be set to an *array* of single-key o
.. note:: .. note::
PostgREST provided headers such as ``Content-Type``, ``Location``, etc. can be overridden this way. Note that irrespective of overridden ``Content-Type`` response header, the content will still be converted to JSON, unless you use :ref:`custom_media`. PostgREST provided headers such as ``Content-Type``, ``Location``, etc. can be overriden this way. Note that irrespective of overridden ``Content-Type`` response header, the content will still be converted to JSON, unless you use :ref:`custom_media`.
.. _guc_resp_status: .. _guc_resp_status:
+4 -4
View File
@@ -1,7 +1,7 @@
# This file is auto-generated by postgrest-nixpkgs-upgrade # This file is auto-generated by postgrest-nixpkgs-upgrade
sphinx==9.1.0 sphinx==7.4.7
sphinx-copybutton==0.5.2 sphinx-copybutton==0.5.2
sphinx-rtd-dark-mode==1.3.0 sphinx-rtd-dark-mode==1.3.0
sphinx-rtd-theme==3.1.0 sphinx-rtd-theme==3.0.2
sphinx-tabs==3.5.0 sphinx-tabs==3.4.7
sphinxext-opengraph==0.13.0 sphinxext-opengraph==0.9.1
+1 -1
View File
@@ -22,7 +22,7 @@ Step 1. Install PostgreSQL
If you're already familiar with using PostgreSQL and have it installed on your system you can use the existing installation (see :ref:`pg-dependency` for minimum requirements). For this tutorial we'll describe how to use the database in Docker because database configuration is otherwise too complicated for a simple tutorial. If you're already familiar with using PostgreSQL and have it installed on your system you can use the existing installation (see :ref:`pg-dependency` for minimum requirements). For this tutorial we'll describe how to use the database in Docker because database configuration is otherwise too complicated for a simple tutorial.
If Docker is not installed, you can get it `here <https://www.docker.com/get-started>`_. Make sure that Docker service is `started <https://docs.docker.com/engine/daemon/start/#start-the-daemon-using-operating-system-utilities>`_. Next, let's pull and start the database image: If Docker is not installed, you can get it `here <https://www.docker.com/get-started>`_. Next, let's pull and start the database image:
.. code-block:: bash .. code-block:: bash
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1776949667, "lastModified": 1731165248,
"narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=", "narHash": "sha256-DiHFKIdBmMx5/DUARhVqaxvEIiy4EE6Eqs9Qs4oxme8=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30", "rev": "a90280100f41a10914edfe729a4053e60c92b8e3",
"type": "github" "type": "github"
}, },
"original": { "original": {
+3 -7
View File
@@ -33,8 +33,8 @@
in in
{ {
packages = genSystems (attrs: { packages = genSystems (attrs: {
default = attrs.postgrestPackage.bin; default = attrs.postgrestPackage;
profiled = attrs.postgrestProfiled.bin; profiled = attrs.postgrestProfiled;
} // nixpkgs.lib.optionalAttrs (attrs ? postgrestStatic) { } // nixpkgs.lib.optionalAttrs (attrs ? postgrestStatic) {
static = attrs.postgrestStatic; static = attrs.postgrestStatic;
}); });
@@ -42,13 +42,9 @@
apps = genSystems (attrs: { apps = genSystems (attrs: {
default = { default = {
type = "app"; type = "app";
program = "${attrs.postgrestStatic or attrs.postgrestPackage.bin}/bin/postgrest"; program = "${attrs.postgrestStatic or attrs.postgrestPackage}/bin/postgrest";
meta.description = "REST API for any Postgres database"; meta.description = "REST API for any Postgres database";
}; };
}); });
devShells = genSystems (postgrest: {
default = import ./shell.nix { inherit postgrest; };
});
}; };
} }
+15 -15
View File
@@ -72,10 +72,9 @@ The PostgREST utilities available in `nix-shell` all have names that begin with
```bash ```bash
# Note: The utilities listed here might not be up to date. # Note: The utilities listed here might not be up to date.
[nix-shell]$ postgrest-<tab> [nix-shell]$ postgrest-<tab>
postgrest-build postgrest-parallel-curl postgrest-build postgrest-profiled-run
postgrest-check postgrest-profiled-run postgrest-check postgrest-push-cachix
postgrest-clean postgrest-push-cachix postgrest-clean postgrest-release
postgrest-commitlint postgrest-release
postgrest-coverage postgrest-repl postgrest-coverage postgrest-repl
postgrest-coverage-draft-overlay postgrest-run postgrest-coverage-draft-overlay postgrest-run
postgrest-docs-build postgrest-style postgrest-docs-build postgrest-style
@@ -91,14 +90,15 @@ postgrest-gen-ctags postgrest-watch
postgrest-gen-jwt postgrest-with-all postgrest-gen-jwt postgrest-with-all
postgrest-gen-secret postgrest-with-git postgrest-gen-secret postgrest-with-git
postgrest-git-hooks postgrest-with-pgrst postgrest-git-hooks postgrest-with-pgrst
postgrest-hsie-graph-modules postgrest-with-pg-14 postgrest-hsie-graph-modules postgrest-with-postgresql-12
postgrest-hsie-graph-symbols postgrest-with-pg-15 postgrest-hsie-graph-symbols postgrest-with-postgresql-13
postgrest-hsie-minimal-imports postgrest-with-pg-16 postgrest-hsie-minimal-imports postgrest-with-postgresql-14
postgrest-lint postgrest-with-pg-17 postgrest-lint postgrest-with-postgresql-15
postgrest-loadtest postgrest-with-pg-18 postgrest-loadtest postgrest-with-postgresql-16
postgrest-loadtest-against postgrest-with-slow-pg postgrest-loadtest-against postgrest-with-postgresql-17
postgrest-loadtest-report postgrest-with-slow-postgrest postgrest-loadtest-report postgrest-with-slow-pg
postgrest-nixpkgs-upgrade postgrest-nixpkgs-upgrade postgrest-with-slow-postgrest
postgrest-parallel-curl
... ...
[nix-shell]$ [nix-shell]$
@@ -174,7 +174,7 @@ $ nix-shell --run "postgrest-with-all postgrest-test-spec"
# Run the tests against a specific version of PostgreSQL (use tab-completion in # Run the tests against a specific version of PostgreSQL (use tab-completion in
# nix-shell to see all available versions): # nix-shell to see all available versions):
$ nix-shell --run "postgrest-with-pg-17 postgrest-test-spec" $ nix-shell --run "postgrest-with-postgresql-13 postgrest-test-spec"
``` ```
@@ -284,7 +284,7 @@ Tools like `postgrest-build`, `postgrest-run`, `postgrest-repl` etc. are simple
also run in CI, with the exception of the IO and Memory checks that need to be run also run in CI, with the exception of the IO and Memory checks that need to be run
separately. separately.
`postgrest-with-pg-*` take a command as an argument and will run it `postgrest-with-postgresql-*` take a command as an argument and will run it
with a temporary database. `postgrest-with-all` will run the command against with a temporary database. `postgrest-with-all` will run the command against
all supported PostgreSQL versions. Tests run without `postgrest-with-*` are all supported PostgreSQL versions. Tests run without `postgrest-with-*` are
run against the latest PostgreSQL version by default. run against the latest PostgreSQL version by default.
@@ -380,7 +380,7 @@ that).
We also use `default.nix` to load our pinned version of the `nixpkgs` We also use `default.nix` to load our pinned version of the `nixpkgs`
repository. This set of packages will always be the same, independently from repository. This set of packages will always be the same, independently from
where or when you use it. The pinned version is taken from `flake.lock` and where or when you use it. The pinned version is taken from `flake.lock` and
can be updated with `postgrest-nixpkgs-upgrade`. can be updated with `nix flake update`.
### `shell.nix` ### `shell.nix`
+24 -6
View File
@@ -16,8 +16,11 @@ The following checklist guides you through the complete process in more detail.
## Upgrade the pinned version of `nixpkgs` ## Upgrade the pinned version of `nixpkgs`
The pinned version of [`nixpkgs`](https://github.com/NixOS/nixpkgs) is defined The pinned version of [`nixpkgs`](https://github.com/NixOS/nixpkgs) is defined
in [`flake.nix`](../flake.nix). To upgrade it, you can use a small utility in [`nix/nixpkgs-version.nix`](nixpkgs-version.nix). The pin refers directly to
script defined in [`nix/tools/nixpkgsTools.nix`](tools/nixpkgsTools.nix): a GitHub tarball for the given revision, which is more efficient than pulling
the complete Git repository. To upgrade it to the current `main` of
`nixpkgs`, you can use a small utility script defined in
[`nix/nixpkgs-update.nix`](nixpkgs-update.nix):
```bash ```bash
# From the root of the repository, enter nix-shell # From the root of the repository, enter nix-shell
@@ -27,12 +30,21 @@ nix-shell
postgrest-nixpkgs-upgrade postgrest-nixpkgs-upgrade
# Exit the nix-shell with Ctrl-d # Exit the nix-shell with Ctrl-d
``` ```
## Review overlays ## Review overlays
Check whether the individual [overlays](overlays) are still required. Check whether the individual [overlays](overlays) are still required.
## Check if patches are still required and update them as needed
We track a number of PostgREST-specific patches in [`nix/patches`](patches).
Check whether the pull-requests/issues linked in the
[`default.nix`](patches/default.nix) have progressed and remove/modify the
patches if they did. If conflicting changes occurred, you might have to rebase
the respective patches.
## Build everything ## Build everything
Using the PostgREST binary Nix cache is recommended. Install Using the PostgREST binary Nix cache is recommended. Install
@@ -46,19 +58,25 @@ errors, this is probably due to one of our patches. Try to fix them and re-run
## Update the PostgREST binary cache ## Update the PostgREST binary cache
If you have access to the PostgREST cachix project, you can push the If you have access to the PostgREST cachix signing key, you can push the
artifacts that you built locally to the binary cache. This will accelerate the artifacts that you built locally to the binary cache. This will accelerate the
CI builds and tests, sometimes dramatically. This might sometimes even be CI builds and tests, sometimes dramatically. This might sometimes even be
required to avoid build timeouts in CI. required to avoid build timeouts in CI.
You'll need to login with your token with `cachix authtoken <token>`. You'll need to set the `CACHIX_SIGNING_KEY` before proceeding, e.g. by creating
a file containing `export CACHIX_SIGNING_KEY=...` and sourcing that file, which
avoids having the secret in your shell history.
To push all new artifacts to Cachix, run: To push all new artifacts to Cachix, run:
``` ```
nix-store -qR --include-outputs $$(nix-instantiate) | cachix push postgrest
# Or, equivalently
nix-shell --run postgrest-push-cachix nix-shell --run postgrest-push-cachix
``` ```
The `postgrest-push-cachix` command will query the nix-store to list all The `nix-store` command will query the nix-store to list all dependencies and
dependencies and build artifacts of PostgREST. It will then push build artifacts of PostgREST. The `cachix` command will efficiently push
everything that is not yet cached to the binary cache. everything that is not yet cached to the binary cache.
+7 -20
View File
@@ -4,7 +4,6 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections #-} {-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
-- | Haskell Imports and Exports tool -- | Haskell Imports and Exports tool
@@ -34,16 +33,13 @@ import Data.Function ((&))
import Data.List (intercalate) import Data.List (intercalate)
import Data.Maybe (catMaybes, mapMaybe) import Data.Maybe (catMaybes, mapMaybe)
import Data.Text (Text) import Data.Text (Text)
import GHC.Driver.Errors.Types (GhcMessage)
import GHC.Generics (Generic) import GHC.Generics (Generic)
import GHC.Hs.Extension (GhcPs) import GHC.Hs.Extension (GhcPs)
import GHC.Types.Error (Messages, defaultDiagnosticOpts, import GHC.Types.Error (getMessages)
getMessages)
import GHC.Types.Name.Occurrence (occNameString) import GHC.Types.Name.Occurrence (occNameString)
import GHC.Types.Name.Reader (rdrNameOcc) import GHC.Types.Name.Reader (rdrNameOcc)
import GHC.Unit.Module (moduleNameString) import GHC.Unit.Module.Name (moduleNameString)
import GHC.Utils.Error (pprMsgEnvelopeBagWithLoc) import GHC.Utils.Error (pprMsgEnvelopeBagWithLoc)
import GHC.Utils.Outputable (showSDocUnsafe)
import System.Directory.Recursive (getFilesRecursive) import System.Directory.Recursive (getFilesRecursive)
import System.Exit (exitFailure) import System.Exit (exitFailure)
@@ -202,7 +198,7 @@ sourceSymbols source = do
return $ concatMap (importSymbols source filepath . GHC.unLoc) hsmodImports return $ concatMap (importSymbols source filepath . GHC.unLoc) hsmodImports
-- | Parse a Haskell module -- | Parse a Haskell module
parseModule :: FilePath -> IO (GHC.HsModule GhcPs) parseModule :: FilePath -> IO GHC.HsModule
parseModule filepath = do parseModule filepath = do
result <- ExactPrint.parseModule GHC.Paths.libdir filepath result <- ExactPrint.parseModule GHC.Paths.libdir filepath
case result of case result of
@@ -210,13 +206,7 @@ parseModule filepath = do
return $ GHC.unLoc hsmod return $ GHC.unLoc hsmod
Left errs -> Left errs ->
fail $ "Errors with " <> show filepath <> ":\n " fail $ "Errors with " <> show filepath <> ":\n "
<> formatParseErrors errs <> show (pprMsgEnvelopeBagWithLoc $ getMessages errs)
formatParseErrors :: Messages GhcMessage -> String
formatParseErrors errs =
intercalate "\n "
. fmap showSDocUnsafe
$ pprMsgEnvelopeBagWithLoc (defaultDiagnosticOpts @GhcMessage) (getMessages errs)
-- | Symbols imported in an import declaration. -- | Symbols imported in an import declaration.
-- --
@@ -224,12 +214,9 @@ formatParseErrors errs =
-- only one item is returned. -- only one item is returned.
importSymbols :: FilePath -> FilePath -> GHC.ImportDecl GhcPs -> [ImportedSymbol] importSymbols :: FilePath -> FilePath -> GHC.ImportDecl GhcPs -> [ImportedSymbol]
importSymbols source filepath GHC.ImportDecl{..} = importSymbols source filepath GHC.ImportDecl{..} =
case ideclImportList of case ideclHiding of
Just (importListInterpretation, syms) -> Just (hiding, syms) ->
symbol (if importListInterpretation == GHC.EverythingBut then Hiding else Explicit) symbol (if hiding then Hiding else Explicit) . Just . GHC.unLoc <$> GHC.unLoc syms
. Just
. GHC.unLoc
<$> GHC.unLoc syms
Nothing -> Nothing ->
[ symbol Wildcard Nothing ] [ symbol Wildcard Nothing ]
where where
+61
View File
@@ -0,0 +1,61 @@
# Creating a separate libpq package is is discussed in
# https://github.com/NixOS/nixpkgs/issues/61580, but nixpkgs has not moved
# forward, yet.
# This package is passed to postgresql-libpq (haskell) which needs to be
# cross-compiled to the static build and possibly other architectures as
# as well. To reduce the number of dependencies that need to be built with
# it, this derivation focuses on building the client libraries only. No
# server, no tests.
{ stdenv
, lib
, openssl
, zlib
, postgresql
, pkg-config
, tzdata
}:
stdenv.mkDerivation {
pname = "libpq";
inherit (postgresql) src version patches;
__structuredAttrs = true;
env.CFLAGS = "-fdata-sections -ffunction-sections"
+ (if stdenv.cc.isClang then " -flto" else " -fmerge-constants -Wl,--gc-sections");
configureFlags = [
"--without-gssapi"
"--without-icu"
"--without-readline"
"--with-openssl"
"--with-system-tzdata=${tzdata}/share/zoneinfo"
"--sysconfdir=/etc/postgresql"
];
nativeBuildInputs = [ pkg-config tzdata ];
buildInputs = [ openssl zlib ];
buildFlags = [ "submake-libpq" "submake-libpgport" ];
installPhase = ''
runHook preInstall
make -C src/bin/pg_config install
make -C src/common install
make -C src/include install
make -C src/interfaces/libpq install
make -C src/port install
rm -rfv $out/share
runHook postInstall
'';
outputs = [ "out" ];
meta = with lib; {
homepage = "https://www.postgresql.org";
description = "Client API library for PostgreSQL";
license = licenses.postgresql;
};
}
@@ -6,7 +6,6 @@
, coreutils , coreutils
, git , git
, lib , lib
, moreutils
, runCommand , runCommand
, shellcheck , shellcheck
, stdenv , stdenv
@@ -57,7 +56,7 @@ let
# Example: This way `postgrest-watch -h` will return the help output for watch, while # Example: This way `postgrest-watch -h` will return the help output for watch, while
# `postgrest-watch postgrest-test-spec -h` will return the help output for test-spec. # `postgrest-watch postgrest-test-spec -h` will return the help output for test-spec.
# Taken from: https://github.com/matejak/argbash/issues/114#issuecomment-557108274 # Taken from: https://github.com/matejak/argbash/issues/114#issuecomment-557108274
sed '/_positionals_count + 1/a\\t\t\t\tset -- "''${@:1:1}" "--" "''${@:2}"' $out | ${moreutils}/bin/sponge $out sed '/_positionals_count + 1/a\\t\t\t\tset -- "''${@:1:1}" "--" "''${@:2}"' -i $out
''; '';
bash-completion = bash-completion =
@@ -67,7 +66,7 @@ let
'' ''
+ lib.optionalString (positionalCompletion != "") '' + lib.optionalString (positionalCompletion != "") ''
sed 's#COMPREPLY.*compgen -o bashdefault .*$#${escape positionalCompletion}#' $out | ${moreutils}/bin/sponge $out sed 's#COMPREPLY.*compgen -o bashdefault .*$#${escape positionalCompletion}#' -i $out
'' ''
); );
@@ -104,7 +103,8 @@ let
'' ''
+ lib.optionalString withTmpDir '' + lib.optionalString withTmpDir ''
tmpdir="$(${coreutils}/bin/mktemp -d --tmpdir=/tmp ${name}-XXX)" mkdir -p "''${TMPDIR:-/tmp}/postgrest"
tmpdir="$(${coreutils}/bin/mktemp -d --tmpdir postgrest/${name}-XXX)"
# we keep the tmpdir when an error occurs for debugging # we keep the tmpdir when an error occurs for debugging
trap 'echo Temporary directory kept at: $tmpdir' ERR trap 'echo Temporary directory kept at: $tmpdir' ERR
+2
View File
@@ -3,4 +3,6 @@
checked-shell-script = import ./checked-shell-script; checked-shell-script = import ./checked-shell-script;
gitignore = import ./gitignore.nix; gitignore = import ./gitignore.nix;
haskell-packages = import ./haskell-packages.nix; haskell-packages = import ./haskell-packages.nix;
postgresql-libpq = import ./postgresql-libpq.nix;
slocat = import ./slocat.nix;
} }
+26 -32
View File
@@ -47,43 +47,37 @@ let
# - To modify and try packages locally, see "Working with locally modified Haskell packages" in the Nix README. # - To modify and try packages locally, see "Working with locally modified Haskell packages" in the Nix README.
# Before upgrading fuzzyset to 0.3, check: https://github.com/PostgREST/postgrest/issues/3329 # Before upgrading fuzzyset to 0.3, check: https://github.com/PostgREST/postgrest/issues/3329
# jailbreak, because hspec limit for tests
fuzzyset = prev.fuzzyset_0_2_4; fuzzyset = prev.fuzzyset_0_2_4;
http2 = hasql-pool = lib.dontCheck (prev.callHackageDirect
prev.callHackageDirect {
{ pkg = "hasql-pool";
pkg = "http2"; ver = "1.0.1";
ver = "5.4.0"; sha256 = "sha256-Hf1f7lX0LWkjrb25SDBovCYPRdmUP1H6pAxzi7kT4Gg=";
sha256 = "sha256-PeEWVd61bQ8G7LvfLeXklzXqNJFaAjE2ecRMWJZESPE="; }
} { });
{ };
http-semantics = hasql-notifications = lib.dontCheck (prev.callHackageDirect
prev.callHackageDirect {
{ pkg = "hasql-notifications";
pkg = "http-semantics"; ver = "0.2.2.2";
ver = "0.4.0"; sha256 = "sha256-myKwlug7OgTa/qP6mHfCD+5Q8IhM17JvpJBfSo+M01k=";
sha256 = "sha256-rh0z51EKvsu5rQd5n2z3fSRjjEObouNZSBPO9NFYOF0="; }
} { });
{ };
network-run = # newer nixpkgs already has 0.10., so we fallback to default for forward compat
prev.callHackageDirect jose-jwt = prev.jose-jwt_0_10_0 or prev.jose-jwt;
{
pkg = "network-run";
ver = "0.5.0";
sha256 = "sha256-vbXh+CzxDsGApjqHxCYf/ijpZtUCApFbkcF5gyN0THU=";
}
{ };
warp = postgresql-libpq = lib.dontCheck (prev.callHackageDirect
lib.dontCheck (prev.callHackageDirect {
{ pkg = "postgresql-libpq";
pkg = "warp"; ver = "0.10.1.0";
ver = "3.4.13"; sha256 = "sha256-tXOMqCO8opMilI9rx0D+njqjIjbZsH168Bzb8Aq8Ff4=";
sha256 = "sha256-jmr8kpeSPDkOhT0i9PhozZapX4nUs92cOX7POAGb7/M="; }
} {
{ }); postgresql = super.libpq;
});
}; };
in in
{ {
+7
View File
@@ -0,0 +1,7 @@
_: super:
{
# Depending on which nixpkgs version is pinned, libpq might either be available already - or not.
libpq = super.libpq or (super.callPackage ../libpq.nix {
postgresql = super.postgresql_16;
});
}
+13
View File
@@ -0,0 +1,13 @@
_: prev:
{
slocat = prev.buildGoModule {
name = "slocat";
src = prev.fetchFromGitHub {
owner = "robx";
repo = "slocat";
rev = "52e7512c6029fd00483e41ccce260a3b4b9b3b64";
sha256 = "sha256-qn6luuh5wqREu3s8RfuMCP5PKdS2WdwPrujRYTpfzQ8=";
};
vendorHash = null;
};
}
+53 -13
View File
@@ -8,25 +8,65 @@ let
inherit (pkgs) pkgsStatic; inherit (pkgs) pkgsStatic;
inherit (pkgsStatic.haskell) lib; inherit (pkgsStatic.haskell) lib;
packagesStatic = pkgsStatic.haskell.packages.native-bignum."${compiler}"; packagesStatic =
pkgsStatic.haskell.packages."${compiler}".override (old: {
ghc = pkgsStatic.pkgsBuildHost.haskell.compiler."${compiler}".override {
# Using the bundled libffi generally works better for cross-compiling
libffi = null;
# Building sphinx fails on some platforms
enableDocs = false;
# Cross compiling with native bignum works better than with gmp
enableNativeBignum = true;
};
overrides = pkgs.lib.composeExtensions old.overrides (_: prev: {
postgresql-libpq = (lib.overrideCabal prev.postgresql-libpq {
# TODO: This section can be simplified when this PR has made it's way to us:
# https://github.com/NixOS/nixpkgs/pull/286370
# Additionally, we need to use the default version in nixpkgs, otherwise the
# override will not be active as well.
# Using use-pkg-config flag, because pg_config won't work when cross-compiling
configureFlags = [ "-fuse-pkg-config" ];
# postgresql doesn't build in the fully static overlay - but the default
# derivation is built with static libraries anyway.
libraryPkgconfigDepends = [ pkgsStatic.libpq ];
librarySystemDepends = [ ];
}).overrideAttrs (_: prevAttrs: {
buildInputs = prevAttrs.buildInputs ++ [ pkgsStatic.openssl ];
});
});
});
makeExecutableStatic = drv: pkgs.lib.pipe drv [ makeExecutableStatic = drv: pkgs.lib.pipe drv [
lib.compose.justStaticExecutables lib.compose.justStaticExecutables
# To successfully compile a redistributable, fully static executable we need to: # To successfully compile a redistributable, fully static executable we need to:
# 1. avoid any references to /nix/store to prevent blowing up the closure size. # 1. make executable really statically linked.
(drv: drv.overrideAttrs { # 2. avoid any references to /nix/store to prevent blowing up the closure size.
allowedReferences = [ # 3. be able to run the executable.
pkgsStatic.openssl.etc # When checking for references, we ignore the following:
]; # - eeee... are removed references which don't actually exist
}) # - openssl-etc references are purposely designed to be very small
(lib.compose.overrideCabal (drv: {
postFixup = drv.postFixup + ''
exe="$out/bin/postgrest"
# 2. be able to run the executable. if ! (file "$exe" | grep 'statically linked') then
(drv: drv.overrideAttrs { echo "not a static executable, ldd output:"
passthru.tests.version = pkgsStatic.testers.testVersion { ldd "$exe"
package = drv; exit 1
}; fi
})
echo "Checking for references to /nix/store..."
(${pkgsStatic.binutils}/bin/strings "$exe" \
| grep -v /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee \
| grep -v -etc/etc/ssl \
| grep /nix/store || exit 0 && exit 1)
echo "No references to /nix/store found"
"$exe" --help
'';
}))
]; ];
in in
+5 -16
View File
@@ -15,6 +15,7 @@ let
withEnv = postgrest.env; withEnv = postgrest.env;
} }
'' ''
${cabal-install}/bin/cabal v2-update
exec ${cabal-install}/bin/cabal v2-build ${devCabalOptions} "''${_arg_leftovers[@]}" exec ${cabal-install}/bin/cabal v2-build ${devCabalOptions} "''${_arg_leftovers[@]}"
''; '';
@@ -33,17 +34,6 @@ let
exec ${cabal-install}/bin/cabal v2-clean exec ${cabal-install}/bin/cabal v2-clean
''; '';
update =
checkedShellScript
{
name = "postgrest-cabal-update";
docs = "Update cabal's package list from hackage.haskell.org";
workingDir = "/";
}
''
exec ${cabal-install}/bin/cabal v2-update
'';
run = run =
checkedShellScript checkedShellScript
{ {
@@ -55,7 +45,6 @@ let
"ARG_USE_ENV([PGRST_DB_POOL], [1], [PostgREST pool size])" "ARG_USE_ENV([PGRST_DB_POOL], [1], [PostgREST pool size])"
"ARG_USE_ENV([PGRST_DB_POOL_ACQUISITION_TIMEOUT], [1], [PostgREST pool timeout])" "ARG_USE_ENV([PGRST_DB_POOL_ACQUISITION_TIMEOUT], [1], [PostgREST pool timeout])"
"ARG_USE_ENV([PGRST_JWT_SECRET], [reallyreallyreallyreallyverysafe], [PostgREST JWT secret])" "ARG_USE_ENV([PGRST_JWT_SECRET], [reallyreallyreallyreallyverysafe], [PostgREST JWT secret])"
"ARG_USE_ENV([PGRST_ADMIN_SERVER_PORT], [3001], [PostgREST admin server port])"
"ARG_LEFTOVERS([PostgREST arguments])" "ARG_LEFTOVERS([PostgREST arguments])"
]; ];
workingDir = "/"; workingDir = "/";
@@ -66,7 +55,6 @@ let
export PGRST_DB_POOL export PGRST_DB_POOL
export PGRST_DB_POOL_ACQUISITION_TIMEOUT export PGRST_DB_POOL_ACQUISITION_TIMEOUT
export PGRST_JWT_SECRET export PGRST_JWT_SECRET
export PGRST_ADMIN_SERVER_PORT
exec ${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \ exec ${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \
postgrest "''${_arg_leftovers[@]}" postgrest "''${_arg_leftovers[@]}"
@@ -95,8 +83,10 @@ let
export PGRST_DB_POOL_ACQUISITION_TIMEOUT export PGRST_DB_POOL_ACQUISITION_TIMEOUT
export PGRST_JWT_SECRET export PGRST_JWT_SECRET
exec ${cabal-install}/bin/cabal --builddir="dist-prof" v2-run --enable-profiling --disable-shared exe:postgrest -- \ ${cabal-install}/bin/cabal v2-update
+RTS -p -h -RTS "''${_arg_leftovers[@]}" ${cabal-install}/bin/cabal --builddir="dist-prof" v2-build --enable-profiling --disable-shared exe:postgrest
${cabal-install}/bin/cabal --builddir="dist-prof" v2-run -- \
postgrest +RTS -p -h -RTS "''${_arg_leftovers[@]}"
''; '';
repl = repl =
@@ -119,7 +109,6 @@ buildToolbox
inherit inherit
build build
clean clean
update
run run
runProfiled runProfiled
repl; repl;
+5 -10
View File
@@ -10,7 +10,6 @@
, hsie , hsie
, nix , nix
, silver-searcher , silver-searcher
, stdenv
, style , style
, tests , tests
, withTools , withTools
@@ -54,14 +53,10 @@ let
Requires authentication with `cachix authtoken ...`. Requires authentication with `cachix authtoken ...`.
''; '';
args =
[
"ARG_OPTIONAL_SINGLE([system], , [System], [${stdenv.system}])"
];
workingDir = "/"; workingDir = "/";
} }
'' ''
${nix}/bin/nix-instantiate --argstr system "$_arg_system" \ ${nix}/bin/nix-instantiate \
| xargs ${nix}/bin/nix-store -qR --include-outputs \ | xargs ${nix}/bin/nix-store -qR --include-outputs \
| ${cachix}/bin/cachix push postgrest | ${cachix}/bin/cachix push postgrest
''; '';
@@ -82,7 +77,6 @@ let
} }
'' ''
${tests}/bin/postgrest-test-spec ${tests}/bin/postgrest-test-spec
${tests}/bin/postgrest-test-observability
${tests}/bin/postgrest-test-doctests ${tests}/bin/postgrest-test-doctests
${tests}/bin/postgrest-test-io ${tests}/bin/postgrest-test-io
${tests}/bin/postgrest-test-big-schema ${tests}/bin/postgrest-test-big-schema
@@ -172,7 +166,7 @@ let
# The following unsets all GIT_ variables. # The following unsets all GIT_ variables.
unset "''${!GIT_@}" unset "''${!GIT_@}"
# shellcheck disable=SC2329 # shellcheck disable=SC2317
function restore () { function restore () {
ref="$(git stash list --format=format:%gD --grep "$1" -n1)" ref="$(git stash list --format=format:%gD --grep "$1" -n1)"
# this will avoid merge conflicts when applying the stash # this will avoid merge conflicts when applying the stash
@@ -205,7 +199,7 @@ let
${git}/bin/git add . ${git}/bin/git add .
;; ;;
pre-push) pre-push)
# Create a clean working tree without any uncommitted changes. # Create a clean working tree without any uncomitted changes.
${withTools.withGit} HEAD ${style}/bin/postgrest-lint ${withTools.withGit} HEAD ${style}/bin/postgrest-lint
;; ;;
esac esac
@@ -232,7 +226,7 @@ let
${style}/bin/postgrest-lint ${style}/bin/postgrest-lint
;; ;;
pre-push) pre-push)
# Create a clean working tree without any uncommitted changes. # Create a clean working tree without any uncomitted changes.
${withTools.withGit} HEAD ${check} ${withTools.withGit} HEAD ${check}
;; ;;
esac esac
@@ -252,6 +246,7 @@ let
} }
'' ''
mkdir -p "$_arg_dumpdir" mkdir -p "$_arg_dumpdir"
${cabal-install}/bin/cabal v2-update
${cabal-install}/bin/cabal v2-build ${devCabalOptions} \ ${cabal-install}/bin/cabal v2-build ${devCabalOptions} \
--builddir="$tmpdir" \ --builddir="$tmpdir" \
--ghc-option=-ddump-minimal-imports \ --ghc-option=-ddump-minimal-imports \
+23 -19
View File
@@ -2,6 +2,7 @@
, aspellDicts , aspellDicts
, buildToolbox , buildToolbox
, checkedShellScript , checkedShellScript
, fetchPypi
, lib , lib
, plantuml , plantuml
, python3 , python3
@@ -10,14 +11,26 @@
, writers , writers
}: }:
let let
selectPythonPackages = ps: [ selectPythonPackages = ps:
ps.sphinx let
ps.sphinx-copybutton # TODO: Remove with next nixpkgs update
ps.sphinx-rtd-dark-mode sphinx-rtd-theme = assert ps.sphinx-rtd-theme.version == "2.0.0"; ps.sphinx-rtd-theme.overrideAttrs rec {
ps.sphinx-rtd-theme version = "3.0.2";
ps.sphinx-tabs src = fetchPypi {
ps.sphinxext-opengraph pname = "sphinx_rtd_theme";
]; inherit version;
hash = "sha256-t0V7wl3acjsgsIamcLmVPIWeq2CioD7o6yuyPhduX4U=";
};
};
in
[
ps.sphinx
ps.sphinx-copybutton
(ps.sphinx-rtd-dark-mode.override { inherit sphinx-rtd-theme; })
sphinx-rtd-theme
ps.sphinx-tabs
ps.sphinxext-opengraph
];
requirements = writeTextFile { requirements = writeTextFile {
name = "requirements.txt"; name = "requirements.txt";
@@ -35,15 +48,12 @@ let
workingDir = "/docs"; workingDir = "/docs";
} }
'' ''
# https://github.com/sphinx-doc/sphinx/issues/11739
export LC_ALL=C
function build() { function build() {
${python}/bin/sphinx-build --color -W -a -n . -b "$@" ${python}/bin/sphinx-build --color -W -a -n . -b "$@"
} }
if [ "$_arg_language" == "" ]; then if [ "$_arg_language" == "" ]; then
# clean previous build, otherwise some errors might be suppressed # clean previous build, otherwise some errors might be supressed
rm -rf "../.docs-build/html/default" rm -rf "../.docs-build/html/default"
if [ -d languages ]; then if [ -d languages ]; then
@@ -54,7 +64,7 @@ let
build html "../.docs-build/html/default" build html "../.docs-build/html/default"
else else
# clean previous build, otherwise some errors might be suppressed # clean previous build, otherwise some errors might be supressed
rm -rf "../.docs-build/html/$_arg_language" rm -rf "../.docs-build/html/$_arg_language"
# update and build specific locale, can be used to create new locale # update and build specific locale, can be used to create new locale
@@ -122,8 +132,6 @@ let
workingDir = "/docs"; workingDir = "/docs";
} }
'' ''
export LC_ALL=C
FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ') FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ')
# shellcheck disable=SC2086 disable=SC2016 # shellcheck disable=SC2086 disable=SC2016
@@ -144,8 +152,6 @@ let
workingDir = "/docs"; workingDir = "/docs";
} }
'' ''
export LC_ALL=C
FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ') FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ')
tail -n+2 postgrest.dict \ tail -n+2 postgrest.dict \
@@ -164,8 +170,6 @@ let
workingDir = "/docs"; workingDir = "/docs";
} }
'' ''
export LC_ALL=C
${python}/bin/sphinx-build --color -b linkcheck . ../.docs-build ${python}/bin/sphinx-build --color -b linkcheck . ../.docs-build
''; '';
-52
View File
@@ -1,52 +0,0 @@
# Generate RSA JWK/public material for loadtests.
import argparse
import sys
from pathlib import Path
import jwcrypto.jwk as jwk
def main():
parser = argparse.ArgumentParser(
description="Generate RSA JWK/private key pair for loadtests"
)
parser.add_argument(
"--rsa",
dest="jwk_path",
metavar="JWK_PATH",
type=Path,
required=True,
help="Path to write the RSA JWK file",
)
parser.add_argument(
"--private-key",
dest="private_key_path",
metavar="PRIVATE_KEY_PATH",
type=Path,
required=True,
help="Path to write the RSA private key file",
)
args = parser.parse_args()
key = jwk.JWK.generate(kty="RSA", size=4096)
private_jwk, public_jwk = key.export_private(), key.export_public()
try:
args.jwk_path.write_text(public_jwk)
print(f"Created RSA JWK on {args.jwk_path}")
except OSError as e:
print(f"Error writing to {args.jwk_path}:{e}", file=sys.stderr)
sys.exit(1)
try:
args.private_key_path.write_text(private_jwk)
print(f"Created private key on {args.private_key_path}")
except OSError as e:
print(f"Error writing to {args.private_key_path}:{e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
+51 -184
View File
@@ -1,92 +1,63 @@
# generates a file to be used by the vegeta load testing tool # generates a file to be used by the vegeta load testing tool
# It includes a worst case scenario for the JWT cache:
# - all requests will have a unique JWT so no cache hits
# - all jwts have an expiration that will be long enough to be
# valid at time of request but short enough that already
# validated jwts will expire later during the loadtest run
# - the above guarantees JWT cache purging will happen
# - we want this to track resource consumption in the worst case
# And a more normal scenario where non-expiring JWTs are picked
# from an array
import time import time
import hmac
import hashlib
import base64
import json
import argparse import argparse
import subprocess
import sys import sys
import random import random
import jwt
from typing import Optional
from pathlib import Path
from enum import Enum
SECRET = b"reallyreallyreallyreallyverysafe"
URL = "http://postgrest" URL = "http://postgrest"
JWT_DURATION = 120
secret_key = b"reallyreallyreallyreallyverysafe" TOTAL_TARGETS = 50000 # tuned by hand to reduce result variance
def generate_jwt( def base64url_encode(data: bytes) -> str:
now: int, """URL-safe Base64 encode without padding."""
exp_inc: Optional[int], return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
rsa_private_key: Optional[jwt.algorithms.RSAAlgorithm],
) -> str:
"""Generate an HS256 or RS256 JWT""" def generate_jwt(exp_inc: int) -> str:
"""Generate an HS256 JWT"""
# Header & payload
header = {"alg": "HS256", "typ": "JWT"}
now = int(time.time())
payload = { payload = {
"sub": f"user_{random.getrandbits(32)}", "sub": f"user_{random.getrandbits(32)}",
"iat": now, "iat": now,
"exp": now + exp_inc,
"role": "postgrest_test_author", "role": "postgrest_test_author",
} }
if exp_inc is not None: # Encode to JSON and then to Base64URL
payload["exp"] = now + exp_inc header_b = json.dumps(header, separators=(",", ":")).encode()
payload_b = json.dumps(payload, separators=(",", ":")).encode()
header_b64 = base64url_encode(header_b)
payload_b64 = base64url_encode(payload_b)
if rsa_private_key is None: # Sign (HMACSHA256) the "<header>.<payload>" string
key = secret_key signing_input = f"{header_b64}.{payload_b64}".encode()
alg = "HS256" signature = hmac.new(SECRET, signing_input, hashlib.sha256).digest()
else: signature_b64 = base64url_encode(signature)
key = rsa_private_key
alg = "RS256" return f"{header_b64}.{payload_b64}.{signature_b64}"
return jwt.encode(payload, key, alg)
HTTP_METHODS = ( # We want to ensure 401 Unauthorized responses don't happen during
"GET", # JWT validation, this can happen when the jwt `exp` is too short.
"OPTIONS", # At the same time, we want to ensure the `exp` is not too big,
) # so expires will occur and postgREST will have to clean cached expired JWTs.
def estimate_adequate_jwt_exp_increase(iteration: int) -> int:
# estimated time takes to build and run postgrest itself
build_run_postgrest_time = 2
# estimated time it takes to generate the targets file
file_generation_time = TOTAL_TARGETS // (10**-5)
# estimated exp time so some JWTs will expire
dynamic_exp_inc = iteration // 1000
HttpMethod = Enum( return build_run_postgrest_time + file_generation_time + dynamic_exp_inc
"HttpMethod",
{method: method for method in HTTP_METHODS},
type=str,
module=__name__,
)
def append_targets(lines: list[str], token: str, http_method: HttpMethod):
lines.append(f"{http_method.value} {URL}/authors_only")
lines.append(f"Authorization: Bearer {token}")
lines.append("") # blank line to separate requests
# we use this to chain commands on loadtest.nix
def run_command(command: list[str]):
if not command:
return
if command[0] == "--":
command = command[1:]
if not command:
return
try:
subprocess.run(command, check=True)
except subprocess.CalledProcessError as exc:
print(
f"Error executing command {' '.join(command)}: {exc}",
file=sys.stderr,
)
sys.exit(exc.returncode)
def main(): def main():
@@ -94,133 +65,29 @@ def main():
description="Generate Vegeta targets with unique JWTs" description="Generate Vegeta targets with unique JWTs"
) )
parser.add_argument( parser.add_argument(
"targets_path", "output",
metavar="TARGETS_PATH",
help="Path to write the generated targets file", help="Path to write the generated targets file",
) )
parser.add_argument(
"--private-key",
dest="private_key_path",
metavar="PRIVATE_KEY_PATH",
type=Path,
default=None,
help="Path to the RSA private key file (required when --rsa is used)",
)
parser.add_argument(
"--worst",
dest="worst",
action=argparse.BooleanOptionalAction,
default=False,
help="Generate worst case targets for a JWT cache",
)
parser.add_argument(
"--rsa",
dest="jwk_path",
metavar="JWK_PATH",
type=Path,
default=None,
help="Path to an existing RSA JWK file used for signing tokens",
)
parser.add_argument(
"--method",
dest="http_method",
choices=list(HTTP_METHODS),
required=True,
help="HTTP method for the vegeta targets",
)
parser.add_argument(
"command",
nargs=argparse.REMAINDER,
help="Command (and arguments) to run after generating the targets",
)
args = parser.parse_args() args = parser.parse_args()
rsa_private_key: Optional[jwt.algorithms.RSAAlgorithm] = None lines = []
is_hs = args.jwk_path is None
http_method = HttpMethod(args.http_method)
nsamples = 1000
if is_hs:
ntargets = 200000
else:
# The asymmetric targets take too long to compute so we reduce them
ntargets = 50000
if not is_hs:
if args.private_key_path is None:
parser.error("--rsa requires the --private-key option")
try:
private_key_data = args.private_key_path.read_text()
except OSError as e:
err = (
f"Error reading RSA private key from {args.private_key_path}: "
f"{e}. Generate RSA materials first with gen_rsa_materials.py."
)
print(err, file=sys.stderr)
sys.exit(1)
try:
rsa_private_key = jwt.algorithms.RSAAlgorithm.from_jwk(private_key_data)
except Exception as exc: # broad exception to capture parsing errors
err = (
f"Error loading RSA private key from {args.private_key_path}: " f"{exc}"
)
print(err, file=sys.stderr)
sys.exit(1)
print(f"Generating {ntargets} targets...")
start_time = time.time() start_time = time.time()
now = int(start_time) for i in range(TOTAL_TARGETS):
token = generate_jwt(estimate_adequate_jwt_exp_increase(i))
lines = [] lines.append(f"OPTIONS {URL}/authors_only")
lines.append(f"Authorization: Bearer {token}")
# We want to ensure 401 Unauthorized responses don't happen during lines.append("") # blank line to separate requests
# JWT validation, this can happen when the jwt `exp` is too short.
# At the same time, we want to ensure the `exp` is not too big,
# so expires will occur and postgREST needs to
# clean cached expired JWTs
if args.worst:
# estimated time takes to build and run postgrest itself
build_run_postgrest_time = 2
# estimated time it takes to generate the targets file
# the division numbers are tuned by hand
if is_hs: # hs generation is much faster
gen_time = ntargets // 66666
else: # asymmetric is slower so the time is higher
gen_time = ntargets // 220
# estimated exp time so some JWTs will expire
inc = build_run_postgrest_time + gen_time
for i in range(ntargets):
token = generate_jwt(now, inc + i // 1000, rsa_private_key)
append_targets(lines, token, http_method)
else:
tokens = [generate_jwt(now, None, rsa_private_key) for _ in range(nsamples)]
for i in range(ntargets):
token = random.choice(tokens)
append_targets(lines, token, http_method)
try: try:
with open(args.targets_path, "w") as f: with open(args.output, "w") as f:
f.write("\n".join(lines)) f.write("\n".join(lines))
except IOError as e: except IOError as e:
print(f"Error writing to {args.targets_path}: {e}", file=sys.stderr) print(f"Error writing to {args.output}: {e}", file=sys.stderr)
sys.exit(1) sys.exit(1)
elapsed = time.time() - start_time elapsed = time.time() - start_time
print(f"Created {ntargets} targets", end=" ") print(f"Created {TOTAL_TARGETS} targets in {args.output} ({elapsed:.2f}s)")
print(f"in {args.targets_path} ({elapsed:.2f}s)")
run_command(args.command)
if __name__ == "__main__": if __name__ == "__main__":
-60
View File
@@ -1,60 +0,0 @@
{ buildToolbox
, checkedShellScript
, commitlint
, writeText
}:
let
# Rules format: [<severity>, <"always"/"never">, <value>]
commitlintConfig = writeText "commitlint.config.mjs" ''
export default {
rules: {
"type-enum": [2, "always", [
'add', // Add a new feature
'amend', // To amend an unrealease commit
'change', // Breaking changes
'chore', // Update sponsors, changelog, readme etc
'ci', // CI configuration files and scripts
'docs', // Documentation
'fix', // Bug fix
'nix', // Related to Nix
'perf', // Performance improvements
'refactor', // Refactoring code
'remove', // Remove a feature or fix
'test', // Adding tests
]],
'subject-case': [2, 'never', ['pascal-case', 'start-case']],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
'subject-max-length': [2, 'always', 80],
'subject-min-length': [2, 'always', 5],
'scope-case': [2, 'always', 'lower-case'],
'body-leading-blank': [2, 'always'],
},
};
'';
commitCheck =
checkedShellScript
{
name = "postgrest-commitlint";
docs = "Script to validate commit messages";
workingDir = "/";
args = [
"ARG_OPTIONAL_SINGLE([from],, [commit ref start from], [main])"
"ARG_OPTIONAL_SINGLE([to],, [commit ref end at], [HEAD])"
];
}
''
# Run commitlint with the given configuration
${commitlint}/bin/commitlint --config ${commitlintConfig} --from "$_arg_from" --to "$_arg_to"
'';
in
buildToolbox
{
name = "postgrest-commitlint";
tools = { inherit commitCheck; };
}
+20 -155
View File
@@ -18,8 +18,6 @@ let
]; ];
} }
'' ''
echo "Starting vegeta loadtest..."
# ARG_USE_ENV only adds defaults or docs for environment variables # ARG_USE_ENV only adds defaults or docs for environment variables
# We manually implement a required check here # We manually implement a required check here
# See also: https://github.com/matejak/argbash/issues/80 # See also: https://github.com/matejak/argbash/issues/80
@@ -43,11 +41,8 @@ let
args = [ args = [
"ARG_OPTIONAL_SINGLE([output], [o], [Filename to dump json output to], [./loadtest/result.bin])" "ARG_OPTIONAL_SINGLE([output], [o], [Filename to dump json output to], [./loadtest/result.bin])"
"ARG_OPTIONAL_SINGLE([testdir], [t], [Directory to load tests and fixtures from], [./test/load])" "ARG_OPTIONAL_SINGLE([testdir], [t], [Directory to load tests and fixtures from], [./test/load])"
"ARG_OPTIONAL_SINGLE([kind], [k], [Kind of loadtest], [mixed])" "ARG_OPTIONAL_SINGLE([kind], [k], [Kind of loadtest (mixed: repeat mixed requests, jwt: run once over many requests with unique jwts)], [mixed])"
"ARG_OPTIONAL_SINGLE([method],, [HTTP method used for the jwt loadtests], [OPTIONS])" "ARG_TYPE_GROUP_SET([KIND], [KIND], [kind], [mixed,jwt])"
"ARG_TYPE_GROUP_SET([KIND], [KIND], [kind], [mixed,errors,jwt-hs,jwt-hs-cache,jwt-hs-cache-worst,jwt-rsa,jwt-rsa-cache,jwt-rsa-cache-worst])"
"ARG_TYPE_GROUP_SET([METHOD], [METHOD], [method], [OPTIONS,GET])"
"ARG_OPTIONAL_SINGLE([monitor], [m], [Monitoring file], [./loadtest/result.csv])"
"ARG_LEFTOVERS([additional vegeta arguments])" "ARG_LEFTOVERS([additional vegeta arguments])"
]; ];
workingDir = "/"; workingDir = "/";
@@ -63,127 +58,35 @@ let
export PGRST_DB_TX_END="rollback-allow-override" export PGRST_DB_TX_END="rollback-allow-override"
export PGRST_LOG_LEVEL="crit" export PGRST_LOG_LEVEL="crit"
export PGRST_JWT_SECRET="reallyreallyreallyreallyverysafe" export PGRST_JWT_SECRET="reallyreallyreallyreallyverysafe"
export PGRST_JWT_CACHE_MAX_LIFETIME="86400"
mkdir -p "$(dirname "$_arg_output")" mkdir -p "$(dirname "$_arg_output")"
abs_output="$(realpath "$_arg_output")" abs_output="$(realpath "$_arg_output")"
case "$_arg_kind" in case "$_arg_kind" in
jwt-hs) jwt)
export PGRST_JWT_CACHE_MAX_ENTRIES="0"
${genTargets} "$_arg_testdir"/gen_targets.http
# shellcheck disable=SC2145 # shellcheck disable=SC2145
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \ ${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
${withTools.withPgrst} -m "$_arg_monitor" \ ${withTools.withPgrst} \
${withGenTargets} --method "$_arg_method" "$_arg_testdir"/gen_targets.http \ sh -c "cd \"$_arg_testdir\" && ${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
sh -c "cd \"$_arg_testdir\" && \ ${vegeta}/bin/vegeta report -type=text "$_arg_output"
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
;; ;;
jwt-hs-cache) *)
# shellcheck disable=SC2145
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
${withTools.withPgrst} -m "$_arg_monitor" \
${withGenTargets} --method "$_arg_method" "$_arg_testdir"/gen_targets.http \
sh -c "cd \"$_arg_testdir\" && \
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
;;
jwt-hs-cache-worst)
# shellcheck disable=SC2145
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
${withTools.withPgrst} -m "$_arg_monitor" \
${withGenTargets} --method "$_arg_method" --worst "$_arg_testdir"/gen_targets.http \
sh -c "cd \"$_arg_testdir\" && \
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
;;
jwt-rsa)
export PGRST_JWT_CACHE_MAX_ENTRIES="0"
${genRsaMaterials} --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
# shellcheck disable=SC2145 # shellcheck disable=SC2145
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \ ${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
${withTools.withPgrst} -m "$_arg_monitor" \ ${withTools.withSlowPg} \
${withGenTargets} --method "$_arg_method" --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json "$_arg_testdir"/gen_targets.http \ ${withTools.withPgrst} \
sh -c "cd \"$_arg_testdir\" && \ ${withTools.withSlowPgrst} \
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\"" sh -c "cd \"$_arg_testdir\" && ${runner} -targets targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
${vegeta}/bin/vegeta report -type=text "$_arg_output"
;; ;;
jwt-rsa-cache)
${genRsaMaterials} --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
# shellcheck disable=SC2145
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
${withTools.withPgrst} -m "$_arg_monitor" \
${withGenTargets} --method "$_arg_method" --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json "$_arg_testdir"/gen_targets.http \
sh -c "cd \"$_arg_testdir\" && \
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
;;
jwt-rsa-cache-worst)
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
${genRsaMaterials} --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
# shellcheck disable=SC2145
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
${withTools.withPgrst} -m "$_arg_monitor" \
${withGenTargets} --method "$_arg_method" --worst --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json "$_arg_testdir"/gen_targets.http \
sh -c "cd \"$_arg_testdir\" && \
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
;;
mixed)
# shellcheck disable=SC2145
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
${withTools.withPgrst} -m "$_arg_monitor" \
sh -c "cd \"$_arg_testdir\" && \
${runner} -targets targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
;;
# here we sleep purposefully to check how much memory does the schema cache consume in the final report
errors)
# shellcheck disable=SC2145
${withTools.withPg} -f "$_arg_testdir"/errors.sql \
${withTools.withPgrst} --timeout 2 --sleep 5 -m "$_arg_monitor" \
sh -c "cd \"$_arg_testdir\" && \
${runner} -targets errors.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
;;
esac esac
${vegeta}/bin/vegeta report -type=text "$_arg_output"
if [ "$_arg_kind" != "errors" ]; then
# fail in case 401 happened on jwt loadtests
unauthorized_count="$(${vegeta}/bin/vegeta report -type=json "$_arg_output" \
| ${jq}/bin/jq -r '.status_codes["401"] // 0')"
if [ "$unauthorized_count" -gt 0 ]; then
last_unauthorized_body="$(${vegeta}/bin/vegeta encode "$_arg_output" \
| ${jq}/bin/jq -rn '
reduce inputs as $item (null;
if $item.code == 401 then $item else . end
)
| if . == null then
empty
else
(.body | @base64d)
end
')"
echo "loadtest failed: found $unauthorized_count 401 Unauthorized responses" >&2
if [ -n "$last_unauthorized_body" ]; then
printf '%s\n' "Last 401 response body:" >&2
printf '%s\n' "$last_unauthorized_body" >&2
fi
exit 1
fi
fi
''; '';
loadtestAgainst = loadtestAgainst =
@@ -212,12 +115,11 @@ let
workingDir = "/"; workingDir = "/";
} }
'' ''
# run loadtest for every target
for tgt in "''${_arg_target[@]}"; do for tgt in "''${_arg_target[@]}"; do
cat << EOF cat << EOF
Running "$_arg_kind" loadtest on "$tgt"... Running loadtest on "$tgt"...
EOF EOF
@@ -226,7 +128,7 @@ let
# Save the results in the current working tree, too, # Save the results in the current working tree, too,
# otherwise they'd be lost in the temporary working tree # otherwise they'd be lost in the temporary working tree
# created by withTools.withGit. # created by withTools.withGit.
${withTools.withGit} "$tgt" ${loadtest} -k "$_arg_kind" -m "$PWD/loadtest/$tgt.csv" --output "$PWD/loadtest/$tgt.bin" --testdir "$PWD/test/load" ${withTools.withGit} "$tgt" ${loadtest} -k "$_arg_kind" --output "$PWD/loadtest/$tgt.bin" --testdir "$PWD/test/load"
cat << EOF cat << EOF
@@ -236,15 +138,13 @@ let
done done
# run loadtest once on HEAD
cat << EOF cat << EOF
Running "$_arg_kind" loadtest on HEAD... Running loadtest on HEAD...
EOF EOF
${loadtest} -k "$_arg_kind" -m "$PWD/loadtest/head.csv" --output "$PWD/loadtest/head.bin" --testdir "$PWD/test/load" ${loadtest} -k "$_arg_kind" --output "$PWD/loadtest/head.bin" --testdir "$PWD/test/load"
cat << EOF cat << EOF
@@ -281,7 +181,6 @@ let
pd.read_json(sys.stdin) \ pd.read_json(sys.stdin) \
.set_index('param') \ .set_index('param') \
.drop(['branch', 'earliest', 'end', 'latest']) \ .drop(['branch', 'earliest', 'end', 'latest']) \
.fillna("") \
.convert_dtypes() \ .convert_dtypes() \
.to_markdown(sys.stdout, floatfmt='.0f') .to_markdown(sys.stdout, floatfmt='.0f')
''; '';
@@ -292,50 +191,16 @@ let
{ {
name = "postgrest-loadtest-report"; name = "postgrest-loadtest-report";
docs = "Create a report of all loadtest reports as markdown."; docs = "Create a report of all loadtest reports as markdown.";
args = [
"ARG_OPTIONAL_SINGLE([group], [g], [Marker to group results])"
];
workingDir = "/"; workingDir = "/";
} }
'' ''
marker=''${_arg_group:+"($_arg_group)"}
echo -e "## Loadtest results $marker\n"
find loadtest -type f -iname '*.bin' -exec ${reporter} {} \; \ find loadtest -type f -iname '*.bin' -exec ${reporter} {} \; \
| ${jq}/bin/jq '[paths(scalars) as $path | {param: $path | join("."), (.branch): getpath($path)}]' \ | ${jq}/bin/jq '[paths(scalars) as $path | {param: $path | join("."), (.branch): getpath($path)}]' \
| ${jq}/bin/jq --slurp 'flatten | group_by(.param) | map(add)' \ | ${jq}/bin/jq --slurp 'flatten | group_by(.param) | map(add)' \
| ${toMarkdown} | ${toMarkdown}
echo -e "\n\n## Loadtest elapsed seconds vs CPU/MEM usage $marker\n"
find loadtest -type f -iname '*.csv' \
| sort -m \
| ${mergeMonitorResults}
''; '';
withGenTargets = genTargets = writers.writePython3 "postgrest-gen-loadtest-targets" { } (builtins.readFile ./generate_targets.py);
writers.writePython3 "postgrest-with-gen-loadtest-targets"
{
libraries = [ python3Packages.pyjwt python3Packages.jwcrypto ];
doCheck = false; # postgrest-style conflicts with this
}
(builtins.readFile ./generate_targets.py);
genRsaMaterials =
writers.writePython3 "postgrest-gen-rsa-materials"
{
libraries = [ python3Packages.jwcrypto ];
doCheck = false; # postgrest-style conflicts with this
}
(builtins.readFile ./gen_rsa_materials.py);
mergeMonitorResults =
writers.writePython3 "postgrest-merge-monitor-results"
{
libraries = [ python3Packages.pandas python3Packages.tabulate ];
}
(builtins.readFile ./merge_monitor_result.py);
in in
buildToolbox { buildToolbox {
name = "postgrest-loadtest"; name = "postgrest-loadtest";
-44
View File
@@ -1,44 +0,0 @@
import os
import sys
import pandas as pd
KEY = "Elapsed seconds"
BASE_METRICS = ["CPU (%)", "Real (MB)"]
branch_order = []
merged = None
paths = [p.strip() for p in sys.stdin.read().split() if p.strip()]
for csv_path in paths:
# br is branch (variable shortened to pass linter)
br = os.path.splitext(os.path.basename(csv_path))[0]
branch_order.append(br)
df = pd.read_csv(csv_path)
if KEY not in df.columns:
sys.exit(f"{csv_path} is missing the {KEY} column")
for m in BASE_METRICS:
if m not in df.columns:
sys.exit(f"Error: '{csv_path}' missing required column '{m}'.")
# add branch marker to every metric column
df = df.rename(columns={c: f"{c} [{br}]" for c in df.columns if c != KEY})
# outer join so missing rows appear
merged = df if merged is None else merged.merge(df, on=KEY, how="outer")
# Re-order columns so related metrics are adjacent
ordered_cols = [KEY]
for metric in BASE_METRICS:
for br in branch_order:
col_name = f"{metric} [{br}]"
if col_name in merged.columns:
ordered_cols.append(col_name)
merged = merged[ordered_cols]
# replace nan with empty string
merged = merged.fillna("")
merged.to_markdown(sys.stdout, index=False, tablefmt="github")
-57
View File
@@ -1,57 +0,0 @@
# Monitor a process pid with psutil and emits a CSV.
import sys
import time
import psutil
import pandas as pd
KEY = "Elapsed seconds"
BASE_METRICS = ["CPU (%)", "Real (MB)"]
SAMPLE_INTERVAL_SECS = 1
if len(sys.argv) != 2 or not sys.argv[1].isdigit():
sys.exit(f"Usage: {sys.argv[0]} <PID>")
pid = int(sys.argv[1])
try:
proc = psutil.Process(pid)
except psutil.NoSuchProcess:
sys.exit(f"Error: process {pid} not found.")
print(f"Starting monitoring of {pid} pid", file=sys.stderr)
records = []
start = time.time()
# ignore first result as per docs recommendation
# https://psutil.readthedocs.io/en/latest/#psutil.cpu_percent
proc.cpu_percent(None)
while True:
try:
if not proc.is_running():
break
time.sleep(SAMPLE_INTERVAL_SECS)
elapsed_secs = int(time.time() - start)
cpu = proc.cpu_percent(None)
meminfo = proc.memory_info()
bytes_in_MB = 1024**2
rss_mb = meminfo.rss / bytes_in_MB
records.append(
[
str(elapsed_secs),
f"{cpu:.3f}",
f"{rss_mb:.3f}",
]
)
except psutil.NoSuchProcess:
break
end = time.time()
total_time = end - start
print(f"Finished {pid} pid monitoring in {total_time:.3f}", file=sys.stderr)
cols = [KEY] + BASE_METRICS
df = pd.DataFrame(records, columns=cols, dtype=str)
df.to_csv(sys.stdout, index=False)
-28
View File
@@ -1,28 +0,0 @@
{ buildToolbox
, checkedShellScript
}:
# Utility script for pinning the latest stable version of Nixpkgs.
# Instead of running `nix flake update` manually, we run this script
# to also pin readthedocs dependencies at the same time.
let
upgrade =
checkedShellScript
{
name = "postgrest-nixpkgs-upgrade";
docs = "Pin the newest version of Nixpkgs.";
workingDir = "/";
}
''
nix flake update
echo "# This file is auto-generated by postgrest-nixpkgs-upgrade" > docs/requirements.txt
cat "$(nix-build -A docs.requirements)" >> docs/requirements.txt
'';
in
buildToolbox
{
name = "postgrest-nixpkgs";
tools = { inherit upgrade; };
}
+22 -15
View File
@@ -20,24 +20,27 @@ let
git diff --exit-code HEAD postgrest.cabal > /dev/null git diff --exit-code HEAD postgrest.cabal > /dev/null
trap "" ERR trap "" ERR
# TODO: Support C+D bumps when implementing hackage releases
bump () { bump () {
current_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)" current_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
# shellcheck disable=SC2034 # shellcheck disable=SC2034
IFS=. read -r A B C D <<< "$current_version" IFS=. read -r major minor patch <<< "$current_version"
echo "Current version is $current_version" echo "Current version is $current_version"
case "$1" in case "$1" in
A) major)
new_version="$((A+1)).0" new_version="$((major+1)).0.0"
new_docs_version="$((A+1))" new_docs_version="$((major+1)).0"
;; ;;
B) minor)
new_version="$A.$((B+1))" new_version="$major.$((minor+1)).0"
new_docs_version="$A" new_docs_version="$major.$((minor+1))"
;;
patch)
new_version="$major.$minor.$((patch+1))"
new_docs_version="$major.$minor"
;; ;;
devel) devel)
new_version="$((A+1))" new_version="$major.$((minor+1))"
new_docs_version="devel" new_docs_version="devel"
;; ;;
esac esac
@@ -52,9 +55,13 @@ let
today_date_for_changelog="$(date '+%Y-%m-%d')" today_date_for_changelog="$(date '+%Y-%m-%d')"
if [[ "$current_branch" == "main" ]]; then if [[ "$current_branch" == "main" ]]; then
bump A if [[ "$_arg_major" == "on" ]]; then
bump major
else
bump minor
fi
else else
bump B bump patch
fi fi
echo "Updating CHANGELOG.md ..." echo "Updating CHANGELOG.md ..."
@@ -68,10 +75,10 @@ let
bump devel bump devel
# The order of operations is important here: # The order of operations is important here:
# - bump devel is run and $A is updated to the new version # - bump devel is run and $major is upated to the new version
# - the branch is created with the new A, but the commit before the devel bump # - the branch is created with the new major, but the commit before the devel bump
# - the devel bump is committed # - the devel bump is committed
git branch "v$A" git branch -f "v$major"
echo "Committing (devel bump)..." echo "Committing (devel bump)..."
git commit -m "bump version to $new_version" > /dev/null git commit -m "bump version to $new_version" > /dev/null
@@ -83,7 +90,7 @@ let
if [[ "$current_branch" == "main" ]]; then if [[ "$current_branch" == "main" ]]; then
push1="git push $remote $current_branch" push1="git push $remote $current_branch"
push2="git push $remote v$A" push2="git push $remote v$major --force"
else else
push1="git push $remote $current_branch" push1="git push $remote $current_branch"
push2="" push2=""
+6 -33
View File
@@ -7,12 +7,9 @@
, hlint , hlint
, hsie , hsie
, nixpkgs-fmt , nixpkgs-fmt
, python3Packages
, ruff
, silver-searcher , silver-searcher
, statix , statix
, stylish-haskell , stylish-haskell
, writeText
}: }:
let let
style = style =
@@ -29,20 +26,19 @@ let
# Format Haskell files # Format Haskell files
# --vimgrep fixes a bug in ag: https://github.com/ggreer/the_silver_searcher/issues/753 # --vimgrep fixes a bug in ag: https://github.com/ggreer/the_silver_searcher/issues/753
# TODO: fix style issues in src/protolude and include it ${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' . \
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' --ignore-dir=src/protolude . \
| xargs ${stylish-haskell}/bin/stylish-haskell -i | xargs ${stylish-haskell}/bin/stylish-haskell -i
# Format Python files # Format Python files
${black}/bin/black . 2> /dev/null ${black}/bin/black . 2> /dev/null
''; '';
# Script to check whether any uncommitted changes result from postgrest-style # Script to check whether any uncommited changes result from postgrest-style
styleCheck = styleCheck =
checkedShellScript checkedShellScript
{ {
name = "postgrest-style-check"; name = "postgrest-style-check";
docs = "Check whether postgrest-style results in any uncommitted changes."; docs = "Check whether postgrest-style results in any uncommited changes.";
workingDir = "/"; workingDir = "/";
} }
'' ''
@@ -53,20 +49,6 @@ let
${git}/bin/git diff-index --exit-code HEAD -- '*.hs' '*.lhs' '*.nix' '*.py' ${git}/bin/git diff-index --exit-code HEAD -- '*.hs' '*.lhs' '*.nix' '*.py'
''; '';
hlintConfig = writeText "hlintConfig.yml" ''
# Arguments passed to hlint
- arguments: [-j, -XQuasiQuotes, -XNoPatternSynonyms]
# Warnings
- warn: { lhs: "a == a", rhs: "True", note: "This comparison always evaluates to True" }
- warn: { lhs: "a /= a", rhs: "False", note: "This comparison always evaluates to False" }
- warn: { lhs: "a < a", rhs: "False", note: "This comparison always evaluates to False" }
- warn: { lhs: "a > a", rhs: "False", note: "This comparison always evaluates to False" }
- warn: { lhs: "a <= a", rhs: "True", note: "This comparison always evaluates to True" }
- warn: { lhs: "a >= a", rhs: "True", note: "This comparison always evaluates to True" }
'';
lint = lint =
checkedShellScript checkedShellScript
{ {
@@ -81,22 +63,13 @@ let
echo "Scanning nix files for unused code..." echo "Scanning nix files for unused code..."
${deadnix}/bin/deadnix -f ${deadnix}/bin/deadnix -f
# ruff has gaps in scanning for unused code, so we use vulture
echo "Scanning python files for unused code..."
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?py$' . \
| xargs ${python3Packages.vulture}/bin/vulture --exclude docs/conf.py --min-confidence 80
echo "Linting python files..."
${ruff}/bin/ruff check .
echo "Checking consistency of import aliases in Haskell code..." echo "Checking consistency of import aliases in Haskell code..."
${hsie} check-aliases main src/PostgREST ${hsie} check-aliases main src
echo "Linting Haskell files..." echo "Linting Haskell files..."
# --vimgrep fixes a bug in ag: https://github.com/ggreer/the_silver_searcher/issues/753 # --vimgrep fixes a bug in ag: https://github.com/ggreer/the_silver_searcher/issues/753
# TODO: fix lint issues in src/protolude and include it ${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' . \
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' --ignore-dir=src/protolude . \ | xargs ${hlint}/bin/hlint -X QuasiQuotes -X NoPatternSynonyms
| xargs ${hlint}/bin/hlint --hint=${hlintConfig}
''; '';
in in
+19 -29
View File
@@ -7,6 +7,7 @@
, glibcLocales ? null , glibcLocales ? null
, gnugrep , gnugrep
, hpc-codecov , hpc-codecov
, hostPlatform
, jq , jq
, lib , lib
, postgrest , postgrest
@@ -27,24 +28,11 @@ let
withEnv = postgrest.env; withEnv = postgrest.env;
} }
'' ''
${cabal-install}/bin/cabal v2-update
${withTools.withPg} -f test/spec/fixtures/load.sql \ ${withTools.withPg} -f test/spec/fixtures/load.sql \
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec -- "''${_arg_leftovers[@]}" ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec -- "''${_arg_leftovers[@]}"
''; '';
testObservability =
checkedShellScript
{
name = "postgrest-test-observability";
docs = "Run the Haskell observability test suite.";
args = [ "ARG_LEFTOVERS([hspec arguments])" ];
workingDir = "/";
withEnv = postgrest.env;
}
''
${withTools.withPg} -f test/observability/fixtures/load.sql \
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:observability -- "''${_arg_leftovers[@]}"
'';
testDoctests = testDoctests =
checkedShellScript checkedShellScript
{ {
@@ -54,6 +42,7 @@ let
withEnv = postgrest.env; withEnv = postgrest.env;
} }
'' ''
${cabal-install}/bin/cabal v2-update
# This makes nix-env -iA tests.doctests.bin work. # This makes nix-env -iA tests.doctests.bin work.
export NIX_GHC=${postgrest.env.NIX_GHC} export NIX_GHC=${postgrest.env.NIX_GHC}
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:doctests ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:doctests
@@ -68,6 +57,7 @@ let
withEnv = postgrest.env; withEnv = postgrest.env;
} }
'' ''
${cabal-install}/bin/cabal v2-update
${withTools.withPg} -f test/spec/fixtures/load.sql \ ${withTools.withPg} -f test/spec/fixtures/load.sql \
${runtimeShell} -c " \ ${runtimeShell} -c " \
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec && \ ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec && \
@@ -95,8 +85,9 @@ let
withEnv = postgrest.env; withEnv = postgrest.env;
} }
'' ''
${cabal-install}/bin/cabal v2-update
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest ${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} -f test/io/fixtures/load.sql \ ${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} -f test/io/fixtures.sql \
${ioTestPython}/bin/pytest --ignore=test/io/test_big_schema.py --ignore=test/io/test_replica.py -v test/io "''${_arg_leftovers[@]}" ${ioTestPython}/bin/pytest --ignore=test/io/test_big_schema.py --ignore=test/io/test_replica.py -v test/io "''${_arg_leftovers[@]}"
''; '';
@@ -110,8 +101,9 @@ let
withEnv = postgrest.env; withEnv = postgrest.env;
} }
'' ''
${cabal-install}/bin/cabal v2-update
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest ${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} -f test/io/fixtures/big_schema.sql \ ${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} -f test/io/big_schema.sql \
${ioTestPython}/bin/pytest -v test/io/test_big_schema.py "''${_arg_leftovers[@]}" ${ioTestPython}/bin/pytest -v test/io/test_big_schema.py "''${_arg_leftovers[@]}"
''; '';
@@ -125,8 +117,9 @@ let
withEnv = postgrest.env; withEnv = postgrest.env;
} }
'' ''
${cabal-install}/bin/cabal v2-update
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest ${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} --replica -f test/io/fixtures/replica.sql \ ${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} --replica -f test/io/replica.sql \
${ioTestPython}/bin/pytest -v test/io/test_replica.py "''${_arg_leftovers[@]}" ${ioTestPython}/bin/pytest -v test/io/test_replica.py "''${_arg_leftovers[@]}"
''; '';
@@ -140,6 +133,7 @@ let
withPath = [ jq ]; withPath = [ jq ];
} }
'' ''
${cabal-install}/bin/cabal v2-update
${withTools.withPg} -f test/spec/fixtures/load.sql \ ${withTools.withPg} -f test/spec/fixtures/load.sql \
${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \ ${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \
postgrest --dump-schema postgrest --dump-schema
@@ -158,7 +152,7 @@ let
} }
( (
# required for `hpc markup` in CI; glibcLocales is not available e.g. on Darwin # required for `hpc markup` in CI; glibcLocales is not available e.g. on Darwin
lib.optionalString (stdenv.isLinux && stdenv.hostPlatform.libc == "glibc") '' lib.optionalString (stdenv.isLinux && hostPlatform.libc == "glibc") ''
export LOCALE_ARCHIVE="${glibcLocales}/lib/locale/locale-archive" export LOCALE_ARCHIVE="${glibcLocales}/lib/locale/locale-archive"
'' + '' +
@@ -168,7 +162,8 @@ let
rm -rf coverage/* rm -rf coverage/*
# build once before running all the tests # build once before running all the tests
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest lib:postgrest test:spec test:observability ${cabal-install}/bin/cabal v2-update
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest lib:postgrest test:spec
( (
trap 'echo Found dead code: Check file list above.' ERR ; trap 'echo Found dead code: Check file list above.' ERR ;
@@ -177,31 +172,26 @@ let
# collect all tests # collect all tests
HPCTIXFILE="$tmpdir"/io.tix \ HPCTIXFILE="$tmpdir"/io.tix \
${withTools.withPg} -f test/io/fixtures/load.sql \ ${withTools.withPg} -f test/io/fixtures.sql \
${cabal-install}/bin/cabal v2-exec ${devCabalOptions} -- ${ioTestPython}/bin/pytest --ignore=test/io/test_big_schema.py --ignore=test/io/test_replica.py -v test/io ${cabal-install}/bin/cabal v2-exec ${devCabalOptions} -- ${ioTestPython}/bin/pytest --ignore=test/io/test_big_schema.py --ignore=test/io/test_replica.py -v test/io
HPCTIXFILE="$tmpdir"/big_schema.tix \ HPCTIXFILE="$tmpdir"/big_schema.tix \
${withTools.withPg} -f test/io/fixtures/big_schema.sql \ ${withTools.withPg} -f test/io/big_schema.sql \
${cabal-install}/bin/cabal v2-exec ${devCabalOptions} -- ${ioTestPython}/bin/pytest -v test/io/test_big_schema.py ${cabal-install}/bin/cabal v2-exec ${devCabalOptions} -- ${ioTestPython}/bin/pytest -v test/io/test_big_schema.py
HPCTIXFILE="$tmpdir"/replica.tix \ HPCTIXFILE="$tmpdir"/replica.tix \
${withTools.withPg} --replica -f test/io/fixtures/replica.sql \ ${withTools.withPg} --replica -f test/io/replica.sql \
${cabal-install}/bin/cabal v2-exec ${devCabalOptions} -- ${ioTestPython}/bin/pytest -v test/io/test_replica.py ${cabal-install}/bin/cabal v2-exec ${devCabalOptions} -- ${ioTestPython}/bin/pytest -v test/io/test_replica.py
HPCTIXFILE="$tmpdir"/spec.tix \ HPCTIXFILE="$tmpdir"/spec.tix \
${withTools.withPg} -f test/spec/fixtures/load.sql \ ${withTools.withPg} -f test/spec/fixtures/load.sql \
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec
HPCTIXFILE="$tmpdir"/observability.tix \
${withTools.withPg} -f test/observability/fixtures/load.sql \
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:observability
# Note: No coverage for doctests, as doctests leverage GHCi and GHCi does not support hpc # Note: No coverage for doctests, as doctests leverage GHCi and GHCi does not support hpc
# collect all the tix files # collect all the tix files
${ghc}/bin/hpc sum --union --exclude=Paths_postgrest --output="$tmpdir"/tests.tix \ ${ghc}/bin/hpc sum --union --exclude=Paths_postgrest --output="$tmpdir"/tests.tix \
"$tmpdir"/io*.tix "$tmpdir"/big_schema*.tix "$tmpdir"/replica*.tix "$tmpdir"/spec.tix \ "$tmpdir"/io*.tix "$tmpdir"/big_schema*.tix "$tmpdir"/replica*.tix "$tmpdir"/spec.tix
"$tmpdir"/observability.tix
# prepare the overlay # prepare the overlay
${ghc}/bin/hpc overlay --output="$tmpdir"/overlay.tix test/coverage.overlay ${ghc}/bin/hpc overlay --output="$tmpdir"/overlay.tix test/coverage.overlay
@@ -256,6 +246,7 @@ let
withPath = [ curl ]; withPath = [ curl ];
} }
'' ''
${cabal-install}/bin/cabal v2-update
${cabal-install}/bin/cabal --builddir="dist-prof" v2-build --enable-profiling --disable-shared exe:postgrest ${cabal-install}/bin/cabal --builddir="dist-prof" v2-build --enable-profiling --disable-shared exe:postgrest
${cabal-install}/bin/cabal --builddir="dist-prof" v2-exec -- ${withTools.withPg} -f test/spec/fixtures/load.sql \ ${cabal-install}/bin/cabal --builddir="dist-prof" v2-exec -- ${withTools.withPg} -f test/spec/fixtures/load.sql \
test/memory/memory-tests.sh test/memory/memory-tests.sh
@@ -268,7 +259,6 @@ buildToolbox
tools = { tools = {
inherit inherit
testSpec testSpec
testObservability
testDoctests testDoctests
testSpecIdempotence testSpecIdempotence
testIO testIO
+105 -95
View File
@@ -5,9 +5,8 @@
, lib , lib
, postgresqlVersions , postgresqlVersions
, postgrest , postgrest
, python3Packages , slocat
, writeText , writeText
, writers
}: }:
let let
withTmpDb = withTmpDb =
@@ -24,7 +23,7 @@ let
"ARG_OPTIONAL_SINGLE([fixtures], [f], [SQL file to load fixtures from])" "ARG_OPTIONAL_SINGLE([fixtures], [f], [SQL file to load fixtures from])"
"ARG_POSITIONAL_SINGLE([command], [Command to run])" "ARG_POSITIONAL_SINGLE([command], [Command to run])"
"ARG_LEFTOVERS([command arguments])" "ARG_LEFTOVERS([command arguments])"
"ARG_USE_ENV([PGUSER], [Postgrest_Test_Authenticator], [Authenticator PG role])" # user is written in mixed case to implicitly test that it is being properly quoted in schema cache queries "ARG_USE_ENV([PGUSER], [postgrest_test_authenticator], [Authenticator PG role])"
"ARG_USE_ENV([PGDATABASE], [postgres], [PG database name])" "ARG_USE_ENV([PGDATABASE], [postgres], [PG database name])"
"ARG_USE_ENV([PGRST_DB_SCHEMAS], [test], [Schema to expose])" "ARG_USE_ENV([PGRST_DB_SCHEMAS], [test], [Schema to expose])"
"ARG_USE_ENV([PGTZ], [utc], [Timezone to use])" "ARG_USE_ENV([PGTZ], [utc], [Timezone to use])"
@@ -45,7 +44,7 @@ let
} }
# Avoid starting multiple layers of withTmpDb, but make sure to have the last invocation # Avoid starting multiple layers of withTmpDb, but make sure to have the last invocation
# load fixtures. Otherwise postgrest-with-pg-xx postgrest-test-io would not be possible. # load fixtures. Otherwise postgrest-with-postgresql-xx postgrest-test-io would not be possible.
if ! test -v PGHOST; then if ! test -v PGHOST; then
mkdir -p "$tmpdir"/{db,socket} mkdir -p "$tmpdir"/{db,socket}
@@ -73,13 +72,7 @@ let
>> "$setuplog" >> "$setuplog"
log "Starting the database cluster..." log "Starting the database cluster..."
# Instead of listening on a local port, we will listen on a unix domain socket. # Instead of listening on a local port, we will listen on a unix domain socket.
# NOTE: unix domain socket filename name must remain under max limit.
# On Linux, it's 108 chars (including '\0' terminator)
# On MacOS, it's 104 chars
# See: https://serverfault.com/questions/641347/check-if-a-path-exceeds-maximum-for-unix-domain-socket
pg_ctl -l "$tmpdir/db.log" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $PGHOST -c log_statement=\"all\" " \ pg_ctl -l "$tmpdir/db.log" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $PGHOST -c log_statement=\"all\" " \
>> "$setuplog" >> "$setuplog"
@@ -105,8 +98,7 @@ let
log "Starting replica on $replica_host" log "Starting replica on $replica_host"
# We set a low max_standby_streaming_delay to make the replication conflict fail faster in tests (otherwise it waits for the default 30s) pg_ctl -D "$replica_dir" -l "$replica_dblog" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $replica_host -c log_statement=\"all\" " \
pg_ctl -D "$replica_dir" -l "$replica_dblog" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $replica_host -c log_statement=\"all\" -c max_standby_streaming_delay=\"3s\" " \
>> "$setuplog" >> "$setuplog"
>&2 echo "${commandName}: Replica enabled. You can connect to it with: psql 'postgres:///$PGDATABASE?host=$replica_host' -U postgres" >&2 echo "${commandName}: Replica enabled. You can connect to it with: psql 'postgres:///$PGDATABASE?host=$replica_host' -U postgres"
@@ -117,7 +109,7 @@ let
export PGRST_DB_URI="postgres:///$PGDATABASE?host=$PGREPLICAHOST,$PGHOST" export PGRST_DB_URI="postgres:///$PGDATABASE?host=$PGREPLICAHOST,$PGHOST"
fi fi
# shellcheck disable=SC2329 # shellcheck disable=SC2317
stop () { stop () {
log "Stopping the database cluster..." log "Stopping the database cluster..."
pg_ctl stop --mode=immediate >> "$setuplog" pg_ctl stop --mode=immediate >> "$setuplog"
@@ -132,11 +124,9 @@ let
fi fi
if test "$_arg_fixtures"; then if test "$_arg_fixtures"; then
load_start=$SECONDS log "Loading fixtures under the postgres role..."
>&2 printf "${commandName}: Loading fixtures under the postgres role..."
psql -U postgres -v PGUSER="$PGUSER" -v ON_ERROR_STOP=1 -f "$_arg_fixtures" >> "$setuplog" psql -U postgres -v PGUSER="$PGUSER" -v ON_ERROR_STOP=1 -f "$_arg_fixtures" >> "$setuplog"
load_end=$((SECONDS - load_start)) log "Done. Running command..."
>&2 printf " done in %ss. Running command...\n" "$load_end"
fi fi
("$_arg_command" "''${_arg_leftovers[@]}") ("$_arg_command" "''${_arg_leftovers[@]}")
@@ -185,6 +175,81 @@ let
withPg = withTmpDb (builtins.head postgresqlVersions); withPg = withTmpDb (builtins.head postgresqlVersions);
withSlowPg =
checkedShellScript
{
name = "postgrest-with-slow-pg";
docs = "Run the given command with simulated high latency postgresql";
args =
[
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
"ARG_LEFTOVERS([command arguments])"
"ARG_USE_ENV([PGHOST], [], [PG host (socket name)])"
"ARG_USE_ENV([PGDELAY], [0ms], [extra PG latency (duration)])"
];
positionalCompletion = "_command";
workingDir = "/";
redirectTixFiles = false;
withTmpDir = true;
}
''
delay="''${PGDELAY:-0ms}"
echo "delaying data to/from postgres by $delay"
REALPGHOST="$PGHOST"
export PGHOST="$tmpdir/socket"
mkdir -p "$PGHOST"
${slocat}/bin/slocat -delay "$delay" -src "$PGHOST/.s.PGSQL.5432" -dst "$REALPGHOST/.s.PGSQL.5432" &
SLOCAT_PID=$!
# shellcheck disable=SC2317
stop_slocat() {
kill "$SLOCAT_PID" || true
wait "$SLOCAT_PID" || true
}
trap stop_slocat EXIT
sleep 1 # should wait for socket file to appear instead
("$_arg_command" "''${_arg_leftovers[@]}")
'';
withSlowPgrst =
checkedShellScript
{
name = "postgrest-with-slow-postgrest";
docs = "Run the given command with simulated high latency postgrest";
args =
[
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
"ARG_LEFTOVERS([command arguments])"
"ARG_USE_ENV([PGRST_SERVER_UNIX_SOCKET], [], [PostgREST host (socket name)])"
"ARG_USE_ENV([PGRST_DELAY], [0ms], [extra PostgREST latency (duration)])"
];
positionalCompletion = "_command";
workingDir = "/";
redirectTixFiles = false;
withTmpDir = true;
}
''
delay="''${PGRST_DELAY:-0ms}"
echo "delaying data to/from PostgREST by $delay"
REAL_PGRST_SERVER_UNIX_SOCKET="$PGRST_SERVER_UNIX_SOCKET"
export PGRST_SERVER_UNIX_SOCKET="$tmpdir/postgrest.socket"
${slocat}/bin/slocat -delay "$delay" -src "$PGRST_SERVER_UNIX_SOCKET" -dst "$REAL_PGRST_SERVER_UNIX_SOCKET" &
SLOCAT_PID=$!
# shellcheck disable=SC2317
stop_slocat() {
kill "$SLOCAT_PID" || true
wait "$SLOCAT_PID" || true
}
trap stop_slocat EXIT
sleep 1 # should wait for socket file to appear instead
("$_arg_command" "''${_arg_leftovers[@]}")
'';
withGit = withGit =
let let
name = "postgrest-with-git"; name = "postgrest-with-git";
@@ -262,38 +327,15 @@ let
done done
''; '';
# Broadcast SIGINT to any running postgrest instances on the host. Uses python for cross-platform compatibility.
signalPostgrest =
writers.writePython3 "postgrest-signal-int"
{ libraries = [ python3Packages.psutil ]; }
''
import psutil
import signal
for proc in psutil.process_iter(["name"]):
try:
if proc.info["name"] == "postgrest":
proc.send_signal(signal.SIGINT)
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
'';
withPgrst = withPgrst =
let
commandName = "postgrest-with-pgrst";
in
checkedShellScript checkedShellScript
{ {
name = commandName; name = "postgrest-with-pgrst";
docs = "Build and run PostgREST and run <command> with PGRST_SERVER_UNIX_SOCKET set."; docs = "Build and run PostgREST and run <command> with PGRST_SERVER_UNIX_SOCKET set.";
args = args =
[ [
"ARG_POSITIONAL_SINGLE([command], [Command to run])" "ARG_POSITIONAL_SINGLE([command], [Command to run])"
"ARG_LEFTOVERS([command arguments])" "ARG_LEFTOVERS([command arguments])"
"ARG_OPTIONAL_SINGLE([monitor], [m], [Enable CPU and memory monitoring of the PostgREST process and output to the designated file as markdown])"
"ARG_OPTIONAL_SINGLE([timeout], [t], [Maximum time to wait for PostgREST to be ready], [5])"
"ARG_OPTIONAL_SINGLE([sleep], [s], [Sleep time after PostgREST is ready, this is useful for monitoring])"
"ARG_USE_ENV([PGRST_CMD], [], [PostgREST executable to run])"
]; ];
positionalCompletion = "_command"; positionalCompletion = "_command";
workingDir = "/"; workingDir = "/";
@@ -303,75 +345,41 @@ let
'' ''
export PGRST_SERVER_UNIX_SOCKET="$tmpdir"/postgrest.socket export PGRST_SERVER_UNIX_SOCKET="$tmpdir"/postgrest.socket
if [ -z "''${PGRST_CMD:-}" ]; then rm -f result
rm -f result if [ -z "''${PGRST_BUILD_CABAL:-}" ]; then
build_start=$SECONDS echo -n "Building postgrest (nix)... "
if [ -z "''${PGRST_BUILD_CABAL:-}" ]; then nix-build -A postgrestPackage > "$tmpdir"/build.log 2>&1 || {
echo -n "${commandName}: Building postgrest (nix)... " echo "failed, output:"
# Using lib.getBin to also make this work with older checkouts, where .bin was not a thing, yet. cat "$tmpdir"/build.log
nix-build -E 'with import ./. {}; pkgs.lib.getBin postgrestPackage' > "$tmpdir"/build.log 2>&1 || { exit 1
echo "failed, output:" }
cat "$tmpdir"/build.log PGRST_CMD=./result/bin/postgrest
exit 1 else
} echo -n "Building postgrest (cabal)... "
PGRST_CMD=$(echo ./result*/bin/postgrest) postgrest-build
else PGRST_CMD=postgrest-run
echo -n "${commandName}: Building postgrest (cabal)... "
postgrest-build
PGRST_CMD=postgrest-run
fi
build_end=$((SECONDS - build_start))
printf "done in %ss.\n" "$build_end"
fi fi
echo "done."
ver=$($PGRST_CMD ${legacyConfig} --version) echo -n "Starting postgrest... "
echo -n "${commandName}: Starting $ver... "
$PGRST_CMD ${legacyConfig} > "$tmpdir"/run.log 2>&1 & $PGRST_CMD ${legacyConfig} > "$tmpdir"/run.log 2>&1 &
pid=$! pid=$!
# shellcheck disable=SC2329 # shellcheck disable=SC2317
cleanup() { cleanup() {
# Send INT to all postgrest processes.
# Workaround to trigger dumping postgrest.prof for postgrest-profiled-run
# Caveat: we cannot realistically limit this to the current process' tree,
# since pkill's --parent supports only direct children; therefore this
# would reap neighbor postgrest instances as well, because INT is asking
# the process to terminate too.
# TODO: consider cgroups to make this cleaner
${signalPostgrest}
kill "$pid" || true kill "$pid" || true
} }
trap cleanup EXIT trap cleanup EXIT
wait_start=$SECONDS timeout -s TERM 5 ${waitForPgrstReady} || {
timeout -s TERM "$_arg_timeout" ${waitForPgrstReady} || {
echo "timed out, output:" echo "timed out, output:"
cat "$tmpdir"/run.log cat "$tmpdir"/run.log
exit 1 exit 1
} }
wait_duration=$((SECONDS - wait_start)) echo "done."
printf "done in %ss.\n" "$wait_duration"
echo "${commandName}: You can tail the server logs with: tail -f $tmpdir/run.log"
if [[ -n "$_arg_monitor" ]]; then
${monitorPid} "$pid" > "$_arg_monitor" &
fi
if [[ -n "$_arg_sleep" ]]; then
sleep "$_arg_sleep"
fi
("$_arg_command" "''${_arg_leftovers[@]}") ("$_arg_command" "''${_arg_leftovers[@]}")
''; '';
monitorPid =
writers.writePython3 "postgrest-monitor-pid"
{
libraries = [ python3Packages.pandas python3Packages.tabulate python3Packages.psutil ];
}
(builtins.readFile ./monitor_pid.py);
in in
buildToolbox buildToolbox
{ {
@@ -380,9 +388,11 @@ buildToolbox
inherit inherit
withGit withGit
withPgAll withPgAll
withPgrst; withPgrst
withSlowPg
withSlowPgrst;
} // builtins.listToAttrs ( } // builtins.listToAttrs (
# Create a `postgrest-with-pg-` for each PostgreSQL version # Create a `postgrest-with-postgresql-` for each PostgreSQL version
builtins.map (pg: { inherit (pg) name; value = withTmpDb pg; }) postgresqlVersions builtins.map (pg: { inherit (pg) name; value = withTmpDb pg; }) postgresqlVersions
); );
# make latest withPg available for other nix files # make latest withPg available for other nix files
+47 -141
View File
@@ -1,31 +1,34 @@
cabal-version: 3.0
name: postgrest name: postgrest
version: 15 version: 13.0.2
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
permits. permits.
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
author: Joe Nelson, Adam Baker, Steve Chavez, Wolfgang Walther author: Joe Nelson, Adam Baker, Steve Chavez
maintainer: Steve Chavez <stevechavezast@gmail.com> maintainer: Steve Chavez <stevechavezast@gmail.com>
category: Executable, PostgreSQL, Network APIs category: Executable, PostgreSQL, Network APIs
homepage: https://postgrest.org homepage: https://postgrest.org
bug-reports: https://github.com/PostgREST/postgrest/issues bug-reports: https://github.com/PostgREST/postgrest/issues
build-type: Simple build-type: Simple
extra-source-files: CHANGELOG.md extra-source-files: CHANGELOG.md
cabal-version: >= 1.10
tested-with: tested-with:
-- stack on FreeBSD
GHC == 9.4.5
-- nix, cabal on Ubuntu (arm)
, GHC == 9.4.8
-- cabal on Ubuntu -- cabal on Ubuntu
-- stack on FreeBSD, MacOS, Ubuntu, Windows -- stack on MacOS, Ubuntu, Windows
, GHC == 9.10.3 , GHC == 9.6.6
-- cabal on Ubuntu -- cabal on Ubuntu
-- nix , GHC == 9.8.2
, GHC == 9.12.3
source-repository head source-repository head
type: git type: git
location: https://github.com/PostgREST/postgrest.git location: git://github.com/PostgREST/postgrest.git
flag dev flag dev
default: False default: False
@@ -46,15 +49,11 @@ library
PostgREST.App PostgREST.App
PostgREST.AppState PostgREST.AppState
PostgREST.Auth PostgREST.Auth
PostgREST.Auth.Jwt
PostgREST.Auth.JwtCache PostgREST.Auth.JwtCache
PostgREST.Auth.Types PostgREST.Auth.Types
PostgREST.Cache.Sieve
PostgREST.CLI PostgREST.CLI
PostgREST.Client
PostgREST.Config PostgREST.Config
PostgREST.Config.Database PostgREST.Config.Database
PostgREST.Debounce
PostgREST.Config.JSPath PostgREST.Config.JSPath
PostgREST.Config.PgVersion PostgREST.Config.PgVersion
PostgREST.Config.Proxy PostgREST.Config.Proxy
@@ -66,23 +65,19 @@ library
PostgREST.SchemaCache.Representations PostgREST.SchemaCache.Representations
PostgREST.SchemaCache.Table PostgREST.SchemaCache.Table
PostgREST.Error PostgREST.Error
PostgREST.Error.Types
PostgREST.Listener PostgREST.Listener
PostgREST.Logger PostgREST.Logger
PostgREST.MainTx
PostgREST.MediaType PostgREST.MediaType
PostgREST.Metrics PostgREST.Metrics
PostgREST.Network PostgREST.Network
PostgREST.Observation PostgREST.Observation
PostgREST.Query PostgREST.Query
PostgREST.Query.PreQuery
PostgREST.Query.QueryBuilder PostgREST.Query.QueryBuilder
PostgREST.Query.SqlFragment PostgREST.Query.SqlFragment
PostgREST.Query.Statements PostgREST.Query.Statements
PostgREST.Plan PostgREST.Plan
PostgREST.Plan.CallPlan PostgREST.Plan.CallPlan
PostgREST.Plan.MutatePlan PostgREST.Plan.MutatePlan
PostgREST.Plan.Negotiate
PostgREST.Plan.ReadPlan PostgREST.Plan.ReadPlan
PostgREST.Plan.Types PostgREST.Plan.Types
PostgREST.RangeQuery PostgREST.RangeQuery
@@ -90,60 +85,61 @@ library
PostgREST.ApiRequest PostgREST.ApiRequest
PostgREST.ApiRequest.Preferences PostgREST.ApiRequest.Preferences
PostgREST.ApiRequest.QueryParams PostgREST.ApiRequest.QueryParams
PostgREST.ApiRequest.Payload
PostgREST.ApiRequest.Types PostgREST.ApiRequest.Types
PostgREST.Response PostgREST.Response
PostgREST.Response.OpenAPI PostgREST.Response.OpenAPI
PostgREST.Response.GucHeader PostgREST.Response.GucHeader
PostgREST.Response.Performance PostgREST.Response.Performance
PostgREST.TimeIt
PostgREST.Version PostgREST.Version
build-depends: base >= 4.9 && < 4.22 other-modules: Paths_postgrest
build-depends: base >= 4.9 && < 4.20
, HTTP >= 4000.3.7 && < 4000.5 , HTTP >= 4000.3.7 && < 4000.5
, Ranged-sets >= 0.3 && < 0.6 , Ranged-sets >= 0.3 && < 0.5
, aeson >= 2.0.3 && < 2.3 , aeson >= 2.0.3 && < 2.3
, auto-update >= 0.1.4 && < 0.3 , auto-update >= 0.1.4 && < 0.2
, base64-bytestring >= 1 && < 1.3 , base64-bytestring >= 1 && < 1.3
, bytestring >= 0.10.8 && < 0.13 , bytestring >= 0.10.8 && < 0.13
, cache >= 0.1.3 && < 0.2.0
, case-insensitive >= 1.2 && < 1.3 , case-insensitive >= 1.2 && < 1.3
, cassava >= 0.4.5 && < 0.6 , cassava >= 0.4.5 && < 0.6
, configurator-pg >= 0.2.11 && < 0.3 , clock >= 0.8.3 && < 0.9.0
, containers >= 0.5.7 && < 0.8 , configurator-pg >= 0.2 && < 0.3
, cookie >= 0.4.2 && < 0.6 , containers >= 0.5.7 && < 0.7
-- crypton 1.1.0 moved from `memory` to `ram`, which jose-jwt fails to build with right now. , cookie >= 0.4.2 && < 0.5
-- should be possible to remove this once jose-jwt had a new release.
, crypton < 1.1.0
, directory >= 1.2.6 && < 1.4 , directory >= 1.2.6 && < 1.4
, either >= 4.4.1 && < 5.1 , either >= 4.4.1 && < 5.1
, extra >= 1.7.0 && < 2.0 , extra >= 1.7.0 && < 2.0
, fuzzyset >= 0.2.4 && < 0.3 , fuzzyset >= 0.2.4 && < 0.3
, hasql >= 1.9 && <= 1.9.3.1 , hasql >= 1.6.1.1 && < 1.7
, hasql-dynamic-statements >= 0.3.1 && <= 0.3.1.8 , hasql-dynamic-statements >= 0.3.1 && < 0.4
, hasql-notifications >= 0.2.4.0 && < 0.3 , hasql-notifications >= 0.2.2.2 && < 0.2.3
, hasql-pool >= 1.1 && <= 1.3.0.4 , hasql-pool >= 1.0.1 && < 1.1
, hasql-transaction >= 1.0.1 && <= 1.2.1 , hasql-transaction >= 1.0.1 && < 1.1
, http-client >= 0.7.19 && < 0.8 , heredoc >= 0.2 && < 0.3
, http-types >= 0.12.2 && < 0.13 , http-types >= 0.12.2 && < 0.13
, insert-ordered-containers >= 0.2.2 && < 0.3 , insert-ordered-containers >= 0.2.2 && < 0.3
, iproute >= 1.7.0 && < 1.8
, jose-jwt >= 0.9.6 && < 0.11 , jose-jwt >= 0.9.6 && < 0.11
, lens >= 4.14 && < 5.4 , lens >= 4.14 && < 5.3
, lens-aeson >= 1.0.1 && < 1.3 , lens-aeson >= 1.0.1 && < 1.3
, mtl >= 2.2.2 && < 2.4 , mtl >= 2.2.2 && < 2.4
, neat-interpolation >= 0.5 && < 0.6 , neat-interpolation >= 0.5 && < 0.6
, network >= 2.6 && < 3.3 , network >= 2.6 && < 3.2
, network-uri >= 2.6.1 && < 2.8 , network-uri >= 2.6.1 && < 2.8
, optparse-applicative >= 0.13 && < 0.19 , optparse-applicative >= 0.13 && < 0.19
, parsec >= 3.1.11 && < 3.2 , parsec >= 3.1.11 && < 3.2
-- Technically unused, can be removed after updating to hasql >= 1.7
, postgresql-libpq >= 0.10 , postgresql-libpq >= 0.10
, prometheus-client >= 1.1.1 && < 1.2.0 , prometheus-client >= 1.1.1 && < 1.2.0
, protolude , protolude >= 0.3.1 && < 0.4
, regex-tdfa >= 1.2.2 && < 1.4 , regex-tdfa >= 1.2.2 && < 1.4
, retry >= 0.7.4 && < 0.10 , retry >= 0.7.4 && < 0.10
, scientific >= 0.3.4 && < 0.4 , scientific >= 0.3.4 && < 0.4
, streaming-commons >= 0.2.3.1 && < 0.3 , streaming-commons >= 0.1.1 && < 0.3
, swagger2 >= 2.4 && < 2.9 , swagger2 >= 2.4 && < 2.9
, text >= 1.2.2 && < 2.2 , text >= 1.2.2 && < 2.2
, time >= 1.6 && < 1.15 , time >= 1.6 && < 1.13
, timeit >= 2.0 && < 2.1
, unordered-containers >= 0.2.8 && < 0.3 , unordered-containers >= 0.2.8 && < 0.3
, unix-compat >= 0.5.4 && < 0.8 , unix-compat >= 0.5.4 && < 0.8
, vault >= 0.3.1.5 && < 0.4 , vault >= 0.3.1.5 && < 0.4
@@ -156,11 +152,7 @@ library
-- for unix sockets; this is tested in test/io/test_io.py. See -- for unix sockets; this is tested in test/io/test_io.py. See
-- https://github.com/kazu-yamamoto/logger/commit/3a71ca70afdbb93d4ecf0083eeba1fbbbcab3fc3 -- https://github.com/kazu-yamamoto/logger/commit/3a71ca70afdbb93d4ecf0083eeba1fbbbcab3fc3
, wai-logger >= 2.4.0 , wai-logger >= 2.4.0
, warp >= 3.4.13 && < 3.5 , warp >= 3.3.19 && < 3.4
, stm >= 2.5 && < 3
, stm-hamt >= 1.2 && < 2
, focus >= 1.0 && < 2
, some >= 1.0.4.1 && < 2
-- -fno-spec-constr may help keep compile time memory use in check, -- -fno-spec-constr may help keep compile time memory use in check,
-- see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304 -- see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304
-- -optP-Wno-nonportable-include-path -- -optP-Wno-nonportable-include-path
@@ -180,64 +172,16 @@ library
build-depends: build-depends:
unix unix
library protolude
visibility: private
default-language: Haskell2010
default-extensions: NoImplicitPrelude
FlexibleContexts
MultiParamTypeClasses
OverloadedStrings
hs-source-dirs: src/protolude
exposed-modules: Protolude
Protolude.Applicative
Protolude.Base
Protolude.Bifunctor
Protolude.Bool
Protolude.CallStack
Protolude.Conv
Protolude.ConvertText
Protolude.Debug
Protolude.Either
Protolude.Error
Protolude.Exceptions
Protolude.Functor
Protolude.List
Protolude.Monad
Protolude.Panic
Protolude.Partial
Protolude.Safe
Protolude.Semiring
Protolude.Show
Protolude.Unsafe
build-depends: array >= 0.4 && < 0.6
, async >= 2.0 && < 2.3
, base >= 4.6 && < 4.22
, bytestring >= 0.10.8 && < 0.13
, containers >= 0.5.7 && < 0.8
, deepseq >= 1.3 && < 1.6
, ghc-prim >= 0.3 && < 0.14
, hashable >= 1.2 && < 1.6
, mtl >= 2.1 && < 2.4
, mtl-compat >= 0.2 && < 0.3
, stm >= 2.5 && < 3
, text >= 1.2.2 && < 2.2
, transformers >= 0.2 && < 0.7
, transformers-compat >= 0.4 && < 0.8
-- Protolude has some partial functions, so
-- it is fine to disable that specific warning
ghc-options: -Werror -Wall -fwarn-identities -Wno-x-partial
-fno-spec-constr -optP-Wno-nonportable-include-path
executable postgrest executable postgrest
default-language: Haskell2010 default-language: Haskell2010
default-extensions: OverloadedStrings default-extensions: OverloadedStrings
NoImplicitPrelude NoImplicitPrelude
hs-source-dirs: main hs-source-dirs: main
main-is: Main.hs main-is: Main.hs
build-depends: base >= 4.9 && < 4.22 build-depends: base >= 4.9 && < 4.20
, containers >= 0.5.7 && < 0.8 , containers >= 0.5.7 && < 0.7
, postgrest , postgrest
, protolude , protolude >= 0.3.1 && < 0.4
ghc-options: -threaded -rtsopts "-with-rtsopts=-N -I0 -qg" ghc-options: -threaded -rtsopts "-with-rtsopts=-N -I0 -qg"
-O2 -Werror -Wall -fwarn-identities -O2 -Werror -Wall -fwarn-identities
-fno-spec-constr -optP-Wno-nonportable-include-path -fno-spec-constr -optP-Wno-nonportable-include-path
@@ -292,9 +236,7 @@ test-suite spec
Feature.Query.PgSafeUpdateSpec Feature.Query.PgSafeUpdateSpec
Feature.Query.PlanSpec Feature.Query.PlanSpec
Feature.Query.PostGISSpec Feature.Query.PostGISSpec
Feature.Query.Preferences.HandlingSpec Feature.Query.PreferencesSpec
Feature.Query.Preferences.MaxAffectedSpec
Feature.Query.Preferences.TimezoneSpec
Feature.Query.QueryLimitedSpec Feature.Query.QueryLimitedSpec
Feature.Query.QuerySpec Feature.Query.QuerySpec
Feature.Query.RangeSpec Feature.Query.RangeSpec
@@ -310,30 +252,28 @@ test-suite spec
Feature.RollbackSpec Feature.RollbackSpec
Feature.RpcPreRequestGucsSpec Feature.RpcPreRequestGucsSpec
SpecHelper SpecHelper
build-depends: base >= 4.9 && < 4.22 build-depends: base >= 4.9 && < 4.20
, aeson >= 2.0.3 && < 2.3 , aeson >= 2.0.3 && < 2.3
, aeson-qq >= 0.8.1 && < 0.9 , aeson-qq >= 0.8.1 && < 0.9
, async >= 2.1.1 && < 2.3 , async >= 2.1.1 && < 2.3
, base64-bytestring >= 1 && < 1.3 , base64-bytestring >= 1 && < 1.3
, bytestring >= 0.10.8 && < 0.13 , bytestring >= 0.10.8 && < 0.13
, case-insensitive >= 1.2 && < 1.3 , case-insensitive >= 1.2 && < 1.3
, containers >= 0.5.7 && < 0.8 , containers >= 0.5.7 && < 0.7
, hasql-pool >= 1.0.1 && <= 1.3.0.4 , hasql-pool >= 1.0.1 && < 1.1
, hasql-transaction >= 1.0.1 && <= 1.2.1 , hasql-transaction >= 1.0.1 && < 1.1
, heredoc >= 0.2 && < 0.3 , heredoc >= 0.2 && < 0.3
, hspec >= 2.3 && < 2.12 , hspec >= 2.3 && < 2.12
, hspec-expectations >= 0.8.4 && < 0.9
, hspec-wai >= 0.10 && < 0.12 , hspec-wai >= 0.10 && < 0.12
, hspec-wai-json >= 0.10 && < 0.12 , hspec-wai-json >= 0.10 && < 0.12
, http-types >= 0.12.3 && < 0.13 , http-types >= 0.12.3 && < 0.13
, jose-jwt >= 0.9.6 && < 0.11 , jose-jwt >= 0.9.6 && < 0.11
, lens >= 4.14 && < 5.4 , lens >= 4.14 && < 5.3
, lens-aeson >= 1.0.1 && < 1.3 , lens-aeson >= 1.0.1 && < 1.3
, monad-control >= 1.0.1 && < 1.1 , monad-control >= 1.0.1 && < 1.1
, postgrest , postgrest
, process >= 1.4.2 && < 1.7 , process >= 1.4.2 && < 1.7
, prometheus-client >= 1.1.1 && < 1.2.0 , protolude >= 0.3.1 && < 0.4
, protolude
, regex-tdfa >= 1.2.2 && < 1.4 , regex-tdfa >= 1.2.2 && < 1.4
, scientific >= 0.3.4 && < 0.4 , scientific >= 0.3.4 && < 0.4
, text >= 1.2.2 && < 2.2 , text >= 1.2.2 && < 2.2
@@ -347,40 +287,6 @@ test-suite spec
-- https://github.com/PostgREST/postgrest/issues/387 -- https://github.com/PostgREST/postgrest/issues/387
-with-rtsopts=-K33K -with-rtsopts=-K33K
test-suite observability
type: exitcode-stdio-1.0
default-language: Haskell2010
default-extensions: OverloadedStrings
QuasiQuotes
NoImplicitPrelude
hs-source-dirs: test/observability
main-is: Main.hs
other-modules: ObsHelper
Observation.JwtCache
Observation.MetricsSpec
Observation.SchemaCacheSpec
build-depends: base >= 4.9 && < 4.22
, base64-bytestring >= 1 && < 1.3
, bytestring >= 0.10.8 && < 0.13
, hasql-pool >= 1.0.1 && <= 1.3.0.4
, hasql-transaction >= 1.0.1 && <= 1.2.1
, hspec >= 2.3 && < 2.12
, hspec-expectations >= 0.8.4 && < 0.9
, 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
, postgrest
, prometheus-client >= 1.1.1 && < 1.2.0
, protolude
, text >= 1.2.2 && < 2.2
, wai >= 3.2.1 && < 3.3
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
-fno-spec-constr -optP-Wno-nonportable-include-path
-fwrite-ide-info
-- https://github.com/PostgREST/postgrest/issues/387
-with-rtsopts=-K33K
test-suite doctests test-suite doctests
type: exitcode-stdio-1.0 type: exitcode-stdio-1.0
default-language: Haskell2010 default-language: Haskell2010
@@ -388,10 +294,10 @@ test-suite doctests
NoImplicitPrelude NoImplicitPrelude
hs-source-dirs: test/doc hs-source-dirs: test/doc
main-is: Main.hs main-is: Main.hs
build-depends: base >= 4.9 && < 4.22 build-depends: base >= 4.9 && < 4.20
, doctest >= 0.8 , doctest >= 0.8
, postgrest , postgrest
, pretty-simple , pretty-simple
, protolude , protolude >= 0.3.1 && < 0.4
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
-fno-spec-constr -optP-Wno-nonportable-include-path -fno-spec-constr -optP-Wno-nonportable-include-path
+6 -7
View File
@@ -7,9 +7,11 @@
# We highly recommend that use the PostgREST binary cache by installing cachix # We highly recommend that use the PostgREST binary cache by installing cachix
# (https://app.cachix.org/) and running `cachix use postgrest`. # (https://app.cachix.org/) and running `cachix use postgrest`.
{ docker ? false { docker ? false
, postgrest ? import ./default.nix { }
}: }:
let let
postgrest =
import ./default.nix { };
inherit (postgrest) pkgs; inherit (postgrest) pkgs;
inherit (pkgs) lib; inherit (pkgs) lib;
@@ -19,9 +21,7 @@ let
postgrest.cabalTools postgrest.cabalTools
postgrest.devTools postgrest.devTools
postgrest.docs postgrest.docs
postgrest.gitTools
postgrest.loadtest postgrest.loadtest
postgrest.nixpkgsTools
postgrest.release postgrest.release
postgrest.style postgrest.style
postgrest.tests postgrest.tests
@@ -35,7 +35,10 @@ lib.overrideDerivation postgrest.env (
buildInputs = buildInputs =
base.buildInputs ++ [ base.buildInputs ++ [
pkgs.cabal-install pkgs.cabal-install
pkgs.cabal2nix
pkgs.git
pkgs.postgresql pkgs.postgresql
pkgs.update-nix-fetchgit
postgrest.hsie.bin postgrest.hsie.bin
] ]
++ toolboxes; ++ toolboxes;
@@ -44,10 +47,6 @@ lib.overrideDerivation postgrest.env (
'' ''
export HISTFILE=.history export HISTFILE=.history
# Bypass proxy for all hosts, it prevents HTTP client failures used in test
# suites. See: https://github.com/PostgREST/postgrest/issues/4633 for more info
export NO_PROXY=*
source ${pkgs.bash-completion}/etc/profile.d/bash_completion.sh source ${pkgs.bash-completion}/etc/profile.d/bash_completion.sh
source ${pkgs.git}/share/git/contrib/completion/git-completion.bash source ${pkgs.git}/share/git/contrib/completion/git-completion.bash
source ${postgrest.hsie.bash-completion} source ${postgrest.hsie.bash-completion}
+19 -16
View File
@@ -1,3 +1,5 @@
{-# LANGUAGE NamedFieldPuns #-}
module PostgREST.Admin module PostgREST.Admin
( runAdmin ( runAdmin
) where ) where
@@ -7,35 +9,36 @@ import qualified Network.HTTP.Types.Status as HTTP
import qualified Network.Wai as Wai import qualified Network.Wai as Wai
import qualified Network.Wai.Handler.Warp as Warp import qualified Network.Wai.Handler.Warp as Warp
import Control.Monad.Extra (whenJust) import Control.Monad.Extra (whenJust)
import Network.Socket hiding (addrFamily)
import Network.Socket
import Network.Socket.ByteString import Network.Socket.ByteString
import PostgREST.AppState (AppState) import PostgREST.AppState (AppState)
import PostgREST.MediaType (MediaType (..), toContentType) import PostgREST.Config (AppConfig (..))
import PostgREST.Metrics (metricsToText) import PostgREST.Metrics (metricsToText)
import PostgREST.Network (resolveSocketToAddress) import PostgREST.Network (resolveHost)
import PostgREST.Observation (Observation (..)) import PostgREST.Observation (Observation (..))
import qualified PostgREST.AppState as AppState import qualified PostgREST.AppState as AppState
import qualified Network.Socket as NS import Protolude
import Protolude
runAdmin :: AppState -> Maybe NS.Socket -> NS.Socket -> Warp.Settings -> IO () runAdmin :: AppState -> Warp.Settings -> IO ()
runAdmin appState maybeAdminSocket socketREST settings = do runAdmin appState settings = do
whenJust maybeAdminSocket $ \adminSocket -> do AppConfig{configAdminServerPort} <- AppState.getConfig appState
address <- resolveSocketToAddress adminSocket whenJust (AppState.getSocketAdmin appState) $ \adminSocket -> do
observer $ AdminStartObs address host <- resolveHost adminSocket
observer $ AdminStartObs host configAdminServerPort
void . forkIO $ Warp.runSettingsSocket settings adminSocket adminApp void . forkIO $ Warp.runSettingsSocket settings adminSocket adminApp
where where
adminApp = admin appState socketREST adminApp = admin appState
observer = AppState.getObserver appState observer = AppState.getObserver appState
-- | PostgREST admin application -- | PostgREST admin application
admin :: AppState.AppState -> NS.Socket -> Wai.Application admin :: AppState.AppState -> Wai.Application
admin appState socketREST req respond = do admin appState req respond = do
isMainAppReachable <- isRight <$> reachMainApp socketREST isMainAppReachable <- isRight <$> reachMainApp (AppState.getSocketREST appState)
isLoaded <- AppState.isLoaded appState isLoaded <- AppState.isLoaded appState
isPending <- AppState.isPending appState isPending <- AppState.isPending appState
@@ -55,7 +58,7 @@ admin appState socketREST req respond = do
respond $ Wai.responseLBS HTTP.status200 [] (maybe mempty JSON.encode sCache) respond $ Wai.responseLBS HTTP.status200 [] (maybe mempty JSON.encode sCache)
["metrics"] -> do ["metrics"] -> do
mets <- metricsToText mets <- metricsToText
respond $ Wai.responseLBS HTTP.status200 [toContentType MTTextPlain] mets -- Content-Type is required for prometheus compliance respond $ Wai.responseLBS HTTP.status200 [] mets
_ -> _ ->
respond $ Wai.responseLBS HTTP.status404 [] mempty respond $ Wai.responseLBS HTTP.status404 [] mempty
+164 -12
View File
@@ -6,30 +6,44 @@ Description : PostgREST functions to translate HTTP request to a domain type cal
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
module PostgREST.ApiRequest module PostgREST.ApiRequest
( ApiRequest(..) ( ApiRequest(..)
, InvokeMethod(..)
, Mutation(..)
, MediaType(..)
, Action(..)
, DbAction(..)
, Payload(..)
, userApiRequest , userApiRequest
, userPreferences , userPreferences
) where ) where
import qualified Data.CaseInsensitive as CI import qualified Data.Aeson as JSON
import qualified Data.HashMap.Strict as HM import qualified Data.Aeson.Key as K
import qualified Data.List.NonEmpty as NonEmptyList import qualified Data.Aeson.KeyMap as KM
import qualified Data.Set as S import qualified Data.ByteString.Char8 as BS
import qualified Data.Text.Encoding as T import qualified Data.ByteString.Lazy as LBS
import qualified Data.CaseInsensitive as CI
import qualified Data.Csv as CSV
import qualified Data.HashMap.Strict as HM
import qualified Data.List.NonEmpty as NonEmptyList
import qualified Data.Map.Strict as M
import qualified Data.Set as S
import qualified Data.Text.Encoding as T
import qualified Data.Vector as V
import Data.Either.Combinators (mapBoth)
import Control.Arrow ((***))
import Data.Aeson.Types (emptyArray, emptyObject)
import Data.List (lookup) import Data.List (lookup)
import Data.Ranged.Ranges (emptyRange, rangeIntersection, import Data.Ranged.Ranges (emptyRange, rangeIntersection,
rangeIsEmpty) rangeIsEmpty)
import Network.HTTP.Types.Header (RequestHeaders, hCookie) import Network.HTTP.Types.Header (RequestHeaders, hCookie)
import Network.HTTP.Types.URI (parseSimpleQuery)
import Network.Wai (Request (..)) import Network.Wai (Request (..))
import Network.Wai.Parse (parseHttpAccept) import Network.Wai.Parse (parseHttpAccept)
import Web.Cookie (parseCookies) import Web.Cookie (parseCookies)
import PostgREST.ApiRequest.Payload (getPayload)
import PostgREST.ApiRequest.QueryParams (QueryParams (..)) import PostgREST.ApiRequest.QueryParams (QueryParams (..))
import PostgREST.ApiRequest.Types (Action (..), DbAction (..),
InvokeMethod (..),
Mutation (..), Payload (..),
RequestBody, Resource (..))
import PostgREST.Config (AppConfig (..), import PostgREST.Config (AppConfig (..),
OpenAPIMode (..)) OpenAPIMode (..))
import PostgREST.Config.Database (TimezoneNames) import PostgREST.Config.Database (TimezoneNames)
@@ -50,6 +64,44 @@ import qualified PostgREST.MediaType as MediaType
import Protolude import Protolude
type RequestBody = LBS.ByteString
data Payload
= ProcessedJSON -- ^ Cached attributes of a JSON payload
{ payRaw :: LBS.ByteString
-- ^ This is the raw ByteString that comes from the request body. We
-- cache this instead of an Aeson Value because it was detected that for
-- large payloads the encoding had high memory usage, see
-- https://github.com/PostgREST/postgrest/pull/1005 for more details
, payKeys :: S.Set Text
-- ^ Keys of the object or if it's an array these keys are guaranteed to
-- be the same across all its objects
}
| ProcessedUrlEncoded { payArray :: [(Text, Text)], payKeys :: S.Set Text }
| RawJSON { payRaw :: LBS.ByteString }
| RawPay { payRaw :: LBS.ByteString }
data InvokeMethod = Inv | InvRead Bool deriving Eq
data Mutation = MutationCreate | MutationDelete | MutationSingleUpsert | MutationUpdate deriving Eq
data Resource
= ResourceRelation Text
| ResourceRoutine Text
| ResourceSchema
data DbAction
= ActRelationRead {dbActQi :: QualifiedIdentifier, actHeadersOnly :: Bool}
| ActRelationMut {dbActQi :: QualifiedIdentifier, actMutation :: Mutation}
| ActRoutine {dbActQi :: QualifiedIdentifier, actInvMethod :: InvokeMethod}
| ActSchemaRead Schema Bool
data Action
= ActDb DbAction
| ActRelationInfo QualifiedIdentifier
| ActRoutineInfo QualifiedIdentifier InvokeMethod
| ActSchemaInfo
{-| {-|
Describes what the user wants to do. This data type is a Describes what the user wants to do. This data type is a
translation of the raw elements of an HTTP request into domain translation of the raw elements of an HTTP request into domain
@@ -64,7 +116,7 @@ data ApiRequest = ApiRequest {
, iPayload :: Maybe Payload -- ^ Data sent by client and used for mutation actions , iPayload :: Maybe Payload -- ^ Data sent by client and used for mutation actions
, iPreferences :: Preferences.Preferences -- ^ Prefer header values , iPreferences :: Preferences.Preferences -- ^ Prefer header values
, iQueryParams :: QueryParams.QueryParams , iQueryParams :: QueryParams.QueryParams
, iColumns :: S.Set FieldName -- ^ parsed columns from &columns parameter and payload , iColumns :: S.Set FieldName -- ^ parsed colums from &columns parameter and payload
, iHeaders :: [(ByteString, ByteString)] -- ^ HTTP request headers , iHeaders :: [(ByteString, ByteString)] -- ^ HTTP request headers
, iCookies :: [(ByteString, ByteString)] -- ^ Request Cookies , iCookies :: [(ByteString, ByteString)] -- ^ Request Cookies
, iPath :: ByteString -- ^ Raw request path , iPath :: ByteString -- ^ Raw request path
@@ -155,7 +207,7 @@ getAction resource schema method =
getSchema :: AppConfig -> RequestHeaders -> ByteString -> Either ApiRequestError (Schema, Bool) getSchema :: AppConfig -> RequestHeaders -> ByteString -> Either ApiRequestError (Schema, Bool)
getSchema AppConfig{configDbSchemas} hdrs method = do getSchema AppConfig{configDbSchemas} hdrs method = do
case profile of case profile of
Just p | p `notElem` configDbSchemas -> Left $ UnacceptableSchema p $ toList configDbSchemas Just p | p `notElem` configDbSchemas -> Left $ UnacceptableSchema $ toList configDbSchemas
| otherwise -> Right (p, True) | otherwise -> Right (p, True)
Nothing -> Right (defaultSchema, length configDbSchemas /= 1) -- if we have many schemas, assume the default schema was negotiated Nothing -> Right (defaultSchema, length configDbSchemas /= 1) -- if we have many schemas, assume the default schema was negotiated
where where
@@ -188,3 +240,103 @@ getRanges method QueryParams{qsRanges} hdrs
-- The only emptyRange allowed is the limit zero range -- The only emptyRange allowed is the limit zero range
isInvalidRange = topLevelRange == emptyRange && not (hasLimitZero limitRange) isInvalidRange = topLevelRange == emptyRange && not (hasLimitZero limitRange)
topLevelRange = fromMaybe allRange $ HM.lookup "limit" ranges -- if no limit is specified, get all the request rows topLevelRange = fromMaybe allRange $ HM.lookup "limit" ranges -- if no limit is specified, get all the request rows
getPayload :: RequestBody -> MediaType -> QueryParams.QueryParams -> Action -> Either ApiRequestError (Maybe Payload, S.Set FieldName)
getPayload reqBody contentMediaType QueryParams{qsColumns} action = do
checkedPayload <- if shouldParsePayload then payload else Right Nothing
let cols = case (checkedPayload, columns) of
(Just ProcessedJSON{payKeys}, _) -> payKeys
(Just ProcessedUrlEncoded{payKeys}, _) -> payKeys
(Just RawJSON{}, Just cls) -> cls
_ -> S.empty
return (checkedPayload, cols)
where
payload :: Either ApiRequestError (Maybe Payload)
payload = mapBoth InvalidBody Just $ case (contentMediaType, isProc) of
(MTApplicationJSON, _) ->
if isJust columns
then Right $ RawJSON reqBody
else note "All object keys must match" . payloadAttributes reqBody
=<< if LBS.null reqBody && isProc
then Right emptyObject
else first BS.pack $
-- Drop parsing error message in favor of generic one (https://github.com/PostgREST/postgrest/issues/2344)
maybe (Left "Empty or invalid json") Right $ JSON.decode reqBody
(MTTextCSV, _) -> do
json <- csvToJson <$> first BS.pack (CSV.decodeByName reqBody)
note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json
(MTUrlEncoded, True) ->
Right $ ProcessedUrlEncoded params (S.fromList $ fst <$> params)
(MTUrlEncoded, False) ->
let paramsMap = HM.fromList $ (identity *** JSON.String) <$> params in
Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (HM.keys paramsMap)
(MTTextPlain, True) -> Right $ RawPay reqBody
(MTTextXML, True) -> Right $ RawPay reqBody
(MTOctetStream, True) -> Right $ RawPay reqBody
(ct, _) -> Left $ "Content-Type not acceptable: " <> MediaType.toMime ct
shouldParsePayload = case action of
ActDb (ActRelationMut _ MutationDelete) -> False
ActDb (ActRelationMut _ _) -> True
ActDb (ActRoutine _ Inv) -> True
_ -> False
columns = case action of
ActDb (ActRelationMut _ MutationCreate) -> qsColumns
ActDb (ActRelationMut _ MutationUpdate) -> qsColumns
ActDb (ActRoutine _ Inv) -> qsColumns
_ -> Nothing
isProc = case action of
ActDb (ActRoutine _ _) -> True
_ -> False
params = (T.decodeUtf8 *** T.decodeUtf8) <$> parseSimpleQuery (LBS.toStrict reqBody)
type CsvData = V.Vector (M.Map Text LBS.ByteString)
{-|
Converts CSV like
a,b
1,hi
2,bye
into a JSON array like
[ {"a": "1", "b": "hi"}, {"a": 2, "b": "bye"} ]
The reason for its odd signature is so that it can compose
directly with CSV.decodeByName
-}
csvToJson :: (CSV.Header, CsvData) -> JSON.Value
csvToJson (_, vals) =
JSON.Array $ V.map rowToJsonObj vals
where
rowToJsonObj = JSON.Object . KM.fromMapText .
M.map (\str ->
if str == "NULL"
then JSON.Null
else JSON.String . T.decodeUtf8 $ LBS.toStrict str
)
payloadAttributes :: RequestBody -> JSON.Value -> Maybe Payload
payloadAttributes raw json =
-- Test that Array contains only Objects having the same keys
case json of
JSON.Array arr ->
case arr V.!? 0 of
Just (JSON.Object o) ->
let canonicalKeys = S.fromList $ K.toText <$> KM.keys o
areKeysUniform = all (\case
JSON.Object x -> S.fromList (K.toText <$> KM.keys x) == canonicalKeys
_ -> False) arr in
if areKeysUniform
then Just $ ProcessedJSON raw canonicalKeys
else Nothing
Just _ -> Nothing
Nothing -> Just emptyPJArray
JSON.Object o -> Just $ ProcessedJSON raw (S.fromList $ K.toText <$> KM.keys o)
-- truncate everything else to an empty array.
_ -> Just emptyPJArray
where
emptyPJArray = ProcessedJSON (JSON.encode emptyArray) S.empty
-138
View File
@@ -1,138 +0,0 @@
-- |
-- Module : PostgREST.ApiRequest.Payload
-- Description : Parser for PostgREST Request Body
--
-- This module is in charge of parsing the request body (payload)
--
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
module PostgREST.ApiRequest.Payload
( getPayload
) where
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Key as K
import qualified Data.Aeson.KeyMap as KM
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Csv as CSV
import qualified Data.HashMap.Strict as HM
import qualified Data.Map.Strict as M
import qualified Data.Set as S
import qualified Data.Text.Encoding as T
import qualified Data.Vector as V
import Control.Arrow ((***))
import Data.Aeson.Types (emptyArray, emptyObject)
import Data.Either.Combinators (mapBoth)
import Network.HTTP.Types.URI (parseSimpleQuery)
import PostgREST.ApiRequest.QueryParams (QueryParams (..))
import PostgREST.ApiRequest.Types
import PostgREST.Error (ApiRequestError (..))
import PostgREST.MediaType (MediaType (..))
import PostgREST.SchemaCache.Identifiers (FieldName)
import qualified PostgREST.MediaType as MediaType
import Protolude
getPayload :: RequestBody -> MediaType -> QueryParams -> Action -> Either ApiRequestError (Maybe Payload, S.Set FieldName)
getPayload reqBody contentMediaType QueryParams{qsColumns} action = do
checkedPayload <- if shouldParsePayload then payload else Right Nothing
let cols = case (checkedPayload, columns) of
(Just ProcessedJSON{payKeys}, _) -> payKeys
(Just ProcessedUrlEncoded{payKeys}, _) -> payKeys
(Just RawJSON{}, Just cls) -> cls
_ -> S.empty
return (checkedPayload, cols)
where
payload :: Either ApiRequestError (Maybe Payload)
payload = mapBoth InvalidBody Just $ case (contentMediaType, isProc) of
(MTApplicationJSON, _) ->
if isJust columns
then Right $ RawJSON reqBody
else note "All object keys must match" . payloadAttributes reqBody
=<< if LBS.null reqBody && isProc
then Right emptyObject
else first BS.pack $
-- Drop parsing error message in favor of generic one (https://github.com/PostgREST/postgrest/issues/2344)
maybe (Left "Empty or invalid json") Right $ JSON.decode reqBody
(MTTextCSV, _) -> do
json <- csvToJson <$> first BS.pack (CSV.decodeByName reqBody)
note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json
(MTUrlEncoded, True) ->
Right $ ProcessedUrlEncoded params (S.fromList $ fst <$> params)
(MTUrlEncoded, False) ->
let paramsMap = HM.fromList $ (identity *** JSON.String) <$> params in
Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (HM.keys paramsMap)
(MTTextPlain, True) -> Right $ RawPay reqBody
(MTTextXML, True) -> Right $ RawPay reqBody
(MTOctetStream, True) -> Right $ RawPay reqBody
(ct, _) -> Left $ "Content-Type not acceptable: " <> MediaType.toMime ct
shouldParsePayload = case action of
ActDb (ActRelationMut _ MutationDelete) -> False
ActDb (ActRelationMut _ _) -> True
ActDb (ActRoutine _ Inv) -> True
_ -> False
columns = case action of
ActDb (ActRelationMut _ MutationCreate) -> qsColumns
ActDb (ActRelationMut _ MutationUpdate) -> qsColumns
ActDb (ActRoutine _ Inv) -> qsColumns
_ -> Nothing
isProc = case action of
ActDb (ActRoutine _ _) -> True
_ -> False
params = (T.decodeUtf8 *** T.decodeUtf8) <$> parseSimpleQuery (LBS.toStrict reqBody)
type CsvData = V.Vector (M.Map Text LBS.ByteString)
{-|
Converts CSV like
a,b
1,hi
2,bye
into a JSON array like
[ {"a": "1", "b": "hi"}, {"a": 2, "b": "bye"} ]
The reason for its odd signature is so that it can compose
directly with CSV.decodeByName
-}
csvToJson :: (CSV.Header, CsvData) -> JSON.Value
csvToJson (_, vals) =
JSON.Array $ V.map rowToJsonObj vals
where
rowToJsonObj = JSON.Object . KM.fromMapText .
M.map (\str ->
if str == "NULL"
then JSON.Null
else JSON.String . T.decodeUtf8 $ LBS.toStrict str
)
payloadAttributes :: RequestBody -> JSON.Value -> Maybe Payload
payloadAttributes raw json =
-- Test that Array contains only Objects having the same keys
case json of
JSON.Array arr ->
case arr V.!? 0 of
Just (JSON.Object o) ->
let canonicalKeys = S.fromList $ K.toText <$> KM.keys o
areKeysUniform = all (\case
JSON.Object x -> S.fromList (K.toText <$> KM.keys x) == canonicalKeys
_ -> False) arr in
if areKeysUniform
then Just $ ProcessedJSON raw canonicalKeys
else Nothing
Just _ -> Nothing
Nothing -> Just emptyPJArray
JSON.Object o -> Just $ ProcessedJSON raw (S.fromList $ K.toText <$> KM.keys o)
-- truncate everything else to an empty array.
_ -> Just emptyPJArray
where
emptyPJArray = ProcessedJSON (JSON.encode emptyArray) S.empty
+1 -6
View File
@@ -19,7 +19,6 @@ module PostgREST.ApiRequest.Preferences
, PreferMaxAffected(..) , PreferMaxAffected(..)
, fromHeaders , fromHeaders
, shouldCount , shouldCount
, shouldExplainCount
, prefAppliedHeader , prefAppliedHeader
) where ) where
@@ -156,7 +155,7 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
listStripPrefix prefix prefList = listToMaybe $ mapMaybe (BS.stripPrefix prefix) prefList listStripPrefix prefix prefList = listToMaybe $ mapMaybe (BS.stripPrefix prefix) prefList
timezonePref = listStripPrefix "timezone=" prefs timezonePref = listStripPrefix "timezone=" prefs
isTimezonePrefAccepted = ((S.member . decodeUtf8 <$> timezonePref) <*> pure acceptedTzNames) == Just True isTimezonePrefAccepted = (S.member <$> (decodeUtf8 <$> timezonePref) <*> pure acceptedTzNames) == Just True
maxAffectedPref = listStripPrefix "max-affected=" prefs >>= readMaybe . BS.unpack maxAffectedPref = listStripPrefix "max-affected=" prefs >>= readMaybe . BS.unpack
@@ -239,10 +238,6 @@ shouldCount :: Maybe PreferCount -> Bool
shouldCount prefCount = shouldCount prefCount =
prefCount == Just ExactCount || prefCount == Just EstimatedCount prefCount == Just ExactCount || prefCount == Just EstimatedCount
shouldExplainCount :: Maybe PreferCount -> Bool
shouldExplainCount prefCount =
prefCount == Just PlannedCount || prefCount == Just EstimatedCount
-- | Whether to commit or roll back transactions. -- | Whether to commit or roll back transactions.
data PreferTransaction data PreferTransaction
= Commit -- ^ Commit transaction - the default. = Commit -- ^ Commit transaction - the default.
+1 -58
View File
@@ -30,69 +30,12 @@ module PostgREST.ApiRequest.Types
, QuantOperator(..) , QuantOperator(..)
, FtsOperator(..) , FtsOperator(..)
, SelectItem(..) , SelectItem(..)
, Payload (..)
, InvokeMethod (..)
, Mutation (..)
, Resource (..)
, DbAction (..)
, Action (..)
, RequestBody
) where ) where
import qualified Data.ByteString.Lazy as LBS import PostgREST.SchemaCache.Identifiers (FieldName)
import qualified Data.Set as S
import PostgREST.SchemaCache.Identifiers (FieldName,
QualifiedIdentifier (..),
Schema)
import Protolude import Protolude
data InvokeMethod = Inv | InvRead Bool
deriving Eq
data Mutation
= MutationCreate
| MutationDelete
| MutationSingleUpsert
| MutationUpdate
deriving Eq
data Resource
= ResourceRelation Text
| ResourceRoutine Text
| ResourceSchema
data DbAction
= ActRelationRead {dbActQi :: QualifiedIdentifier, actHeadersOnly :: Bool}
| ActRelationMut {dbActQi :: QualifiedIdentifier, actMutation :: Mutation}
| ActRoutine {dbActQi :: QualifiedIdentifier, actInvMethod :: InvokeMethod}
| ActSchemaRead Schema Bool
data Action
= ActDb DbAction
| ActRelationInfo QualifiedIdentifier
| ActRoutineInfo QualifiedIdentifier InvokeMethod
| ActSchemaInfo
type RequestBody = LBS.ByteString
data Payload
= ProcessedJSON -- ^ Cached attributes of a JSON payload
{ payRaw :: LBS.ByteString
-- ^ This is the raw ByteString that comes from the request body. We
-- cache this instead of an Aeson Value because it was detected that for
-- large payloads the encoding had high memory usage, see
-- https://github.com/PostgREST/postgrest/pull/1005 for more details
, payKeys :: S.Set Text
-- ^ Keys of the object or if it's an array these keys are guaranteed to
-- be the same across all its objects
}
| ProcessedUrlEncoded { payArray :: [(Text, Text)], payKeys :: S.Set Text }
| RawJSON { payRaw :: LBS.ByteString }
| RawPay { payRaw :: LBS.ByteString }
-- | The value in `/tbl?select=alias:field.aggregateFunction()::cast` -- | The value in `/tbl?select=alias:field.aggregateFunction()::cast`
data SelectItem data SelectItem
= SelectField = SelectField
+59 -146
View File
@@ -9,25 +9,18 @@ Some of its functionality includes:
- Producing HTTP Headers according to RFCs. - Producing HTTP Headers according to RFCs.
- Content Negotiation - Content Negotiation
-} -}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}
module PostgREST.App module PostgREST.App
( postgrest ( postgrest
, run , run
) where ) where
import GHC.IO.Exception (IOErrorType (..))
import System.IO.Error (ioeGetErrorType)
import Control.Monad.Except (liftEither) import Control.Monad.Except (liftEither)
import Control.Monad.Extra (whenJust)
import Data.Either.Combinators (mapLeft, whenLeft) import Data.Either.Combinators (mapLeft, whenLeft)
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
import Data.String (IsString (..)) import Data.String (IsString (..))
import Network.Wai.Handler.Warp (defaultSettings, setHost, import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort,
setOnException, setPort,
setServerName) setServerName)
import qualified Data.Text.Encoding as T import qualified Data.Text.Encoding as T
@@ -42,7 +35,6 @@ import qualified PostgREST.Cors as Cors
import qualified PostgREST.Error as Error import qualified PostgREST.Error as Error
import qualified PostgREST.Listener as Listener import qualified PostgREST.Listener as Listener
import qualified PostgREST.Logger as Logger import qualified PostgREST.Logger as Logger
import qualified PostgREST.MainTx as MainTx
import qualified PostgREST.Plan as Plan import qualified PostgREST.Plan as Plan
import qualified PostgREST.Query as Query import qualified PostgREST.Query as Query
import qualified PostgREST.Response as Response import qualified PostgREST.Response as Response
@@ -51,68 +43,49 @@ import qualified PostgREST.Unix as Unix (installSignalHandlers)
import PostgREST.ApiRequest (ApiRequest (..)) import PostgREST.ApiRequest (ApiRequest (..))
import PostgREST.AppState (AppState) import PostgREST.AppState (AppState)
import PostgREST.Auth.Types (AuthResult (..)) import PostgREST.Auth.Types (AuthResult (..))
import PostgREST.Config (AppConfig (..), LogLevel (..)) import PostgREST.Config (AppConfig (..), LogLevel (..),
LogQuery (..))
import PostgREST.Config.PgVersion (PgVersion (..))
import PostgREST.Error (Error) import PostgREST.Error (Error)
import PostgREST.Network (resolveSocketToAddress) import PostgREST.Network (resolveHost)
import PostgREST.Observation (Observation (..)) import PostgREST.Observation (Observation (..))
import PostgREST.Response.Performance (ServerTiming (..), import PostgREST.Response.Performance (ServerTiming (..),
serverTimingHeader) serverTimingHeader)
import PostgREST.SchemaCache (SchemaCache (..)) import PostgREST.SchemaCache (SchemaCache (..))
import PostgREST.TimeIt (timeItT)
import PostgREST.Version (docsVersion, prettyVersion) import PostgREST.Version (docsVersion, prettyVersion)
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import qualified Data.List as L import qualified Data.List as L
import Data.Streaming.Network (bindPortTCP, import qualified Network.HTTP.Types as HTTP
bindRandomPortTCP) import qualified Network.Socket as NS
import qualified Data.Text as T import Protolude hiding (Handler)
import qualified Network.HTTP.Types as HTTP import System.TimeIt (timeItT)
import qualified Network.HTTP.Types.Header as HTTP (hVary)
import qualified Network.Socket as NS
import PostgREST.Unix (createAndBindDomainSocket)
import Protolude hiding (Handler)
type Handler = ExceptT Error type Handler = ExceptT Error
run :: AppState -> IO () run :: AppState -> IO ()
run appState = do run appState = do
let observer = AppState.getObserver appState
conf@AppConfig{..} <- AppState.getConfig appState conf@AppConfig{..} <- AppState.getConfig appState
AppState.schemaCacheLoader appState -- Loads the initial SchemaCache AppState.schemaCacheLoader appState -- Loads the initial SchemaCache
(mainSocket, adminSocket) <- initSockets conf Unix.installSignalHandlers (AppState.getMainThreadId appState) (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
let closeSockets = do
whenJust adminSocket NS.close
NS.close mainSocket
Unix.installSignalHandlers observer closeSockets (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
Listener.runListener appState Listener.runListener appState
Admin.runAdmin appState adminSocket mainSocket (serverSettings conf) Admin.runAdmin appState (serverSettings conf)
let app = postgrest configLogLevel appState (AppState.schemaCacheLoader appState) let app = postgrest configLogLevel appState (AppState.schemaCacheLoader appState)
do case configServerUnixSocket of
address <- resolveSocketToAddress mainSocket Just path -> do
observer $ AppServerAddressObs address observer $ AppServerUnixObs path
Nothing -> do
port <- NS.socketPort $ AppState.getSocketREST appState
host <- resolveHost $ AppState.getSocketREST appState
observer $ AppServerPortObs (fromJust host) port
Warp.runSettingsSocket (serverSettings conf & setOnException onWarpException) mainSocket app Warp.runSettingsSocket (serverSettings conf) (AppState.getSocketREST appState) app
where
observer = AppState.getObserver appState
onWarpException :: Maybe Wai.Request -> SomeException -> IO ()
onWarpException _ ex =
when (shouldDisplayException ex) $
observer $ WarpServerObs $ show ex
-- Similar to wai defaultShouldDisplayException in
-- https://github.com/yesodweb/wai//blob/8c3882c60f6abe043889fc20c7efd3fa9747fa4a/warp/Network/Wai/Handler/Warp/Settings.hs#L251-L258
-- but without omitting AsyncException since it's important to log for ThreadKilled, StackOverflow and other cases.
-- We want to reuse this to avoid flooding the logs for some transient failure cases.
shouldDisplayException :: SomeException -> Bool
shouldDisplayException se
| Just (_ :: Warp.InvalidRequest) <- fromException se = False
| Just (ioeGetErrorType -> et) <- fromException se, et == ResourceVanished || et == InvalidArgument = False
| otherwise = True
serverSettings :: AppConfig -> Warp.Settings serverSettings :: AppConfig -> Warp.Settings
serverSettings AppConfig{..} = serverSettings AppConfig{..} =
@@ -130,48 +103,42 @@ postgrest logLevel appState connWorker =
Logger.middleware logLevel Auth.getRole $ Logger.middleware logLevel Auth.getRole $
-- fromJust can be used, because the auth middleware will **always** add -- fromJust can be used, because the auth middleware will **always** add
-- some AuthResult to the vault. -- some AuthResult to the vault.
\req respond -> do \req respond -> case fromJust $ Auth.getResult req of
appConf@AppConfig{..} <- AppState.getConfig appState -- the config must be read again because it can reload Left err -> respond $ Error.errorResponseFor err
case fromJust $ Auth.getResult req of Right authResult -> do
Left err -> respond $ Error.errorResponseFor configClientErrorVerbosity err appConf <- AppState.getConfig appState -- the config must be read again because it can reload
Right authResult -> do maybeSchemaCache <- AppState.getSchemaCache appState
maybeSchemaCache <- AppState.getSchemaCache appState pgVer <- AppState.getPgVersion appState
let let
eitherResponse :: IO (Either Error Wai.Response) eitherResponse :: IO (Either Error Wai.Response)
eitherResponse = eitherResponse =
runExceptT $ postgrestResponse appState appConf maybeSchemaCache authResult req runExceptT $ postgrestResponse appState appConf maybeSchemaCache pgVer authResult req
response <- either (Error.errorResponseFor configClientErrorVerbosity) identity <$> eitherResponse response <- either Error.errorResponseFor identity <$> eitherResponse
-- Launch the connWorker when the connection is down. The postgrest -- Launch the connWorker when the connection is down. The postgrest
-- function can respond successfully (with a stale schema cache) before -- function can respond successfully (with a stale schema cache) before
-- the connWorker is done. However, when there's an empty schema cache -- the connWorker is done.
-- postgrest responds with the error `PGRST002`; this means that the schema when (isServiceUnavailable response) connWorker
-- cache is still loading, so we don't launch the connWorker here because resp <- do
-- it would duplicate the loading process, e.g. https://github.com/PostgREST/postgrest/issues/3704 delay <- AppState.getNextDelay appState
-- TODO: this process may be unnecessary when the Listener is enabled. Revisit once https://github.com/PostgREST/postgrest/issues/1766 is done return $ addRetryHint delay response
when (isServiceUnavailable response && isJust maybeSchemaCache) connWorker respond resp
resp <- do
delay <- AppState.getNextDelay appState
return $ addRetryHint delay response
respond resp
postgrestResponse postgrestResponse
:: AppState.AppState :: AppState.AppState
-> AppConfig -> AppConfig
-> Maybe SchemaCache -> Maybe SchemaCache
-> PgVersion
-> AuthResult -> AuthResult
-> Wai.Request -> Wai.Request
-> Handler IO Wai.Response -> Handler IO Wai.Response
postgrestResponse appState conf@AppConfig{..} maybeSchemaCache authResult@AuthResult{..} req = do postgrestResponse appState conf@AppConfig{..} maybeSchemaCache pgVer authResult@AuthResult{..} req = do
let observer = AppState.getObserver appState
sCache <- sCache <-
case maybeSchemaCache of case maybeSchemaCache of
Just sCache -> Just sCache ->
return sCache return sCache
Nothing -> do Nothing ->
lift $ observer SchemaCacheEmptyObs
throwError Error.NoSchemaCacheError throwError Error.NoSchemaCacheError
body <- lift $ Wai.strictRequestBody req body <- lift $ Wai.strictRequestBody req
@@ -180,49 +147,31 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache authResult@AuthRe
timezones = dbTimezones sCache timezones = dbTimezones sCache
prefs = ApiRequest.userPreferences conf req timezones prefs = ApiRequest.userPreferences conf req timezones
(parseTime, apiReq@ApiRequest{..}) <- withTiming $ liftEither . mapLeft Error.ApiRequestErr $ ApiRequest.userApiRequest conf prefs req body (parseTime, apiReq@ApiRequest{..}) <- withTiming $ liftEither . mapLeft Error.ApiRequestError $ ApiRequest.userApiRequest conf prefs req body
(planTime, plan) <- withTiming $ liftEither $ Plan.actionPlan iAction conf apiReq sCache (planTime, plan) <- withTiming $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
let mainQ = Query.mainQuery plan conf apiReq authResult configDbPreRequest let query = Query.query conf authResult apiReq plan sCache pgVer
tx = MainTx.mainTx mainQ conf authResult apiReq plan sCache logSQL = lift . AppState.getObserver appState . DBQuery (Query.getSQLQuery query)
obsQuery s = when configLogQuery $ observer $ QueryObs mainQ s
(txTime, txResult) <- withTiming $ do (queryTime, queryResult) <- withTiming $ do
case tx of case query of
MainTx.NoDbTx r -> pure r Query.NoDbQuery r -> pure r
MainTx.DbTx{..} -> do Query.DbQuery{..} -> do
dbRes <- lift $ AppState.usePool appState (dqTransaction dqIsoLevel dqTxMode $ runExceptT dqDbHandler) dbRes <- lift $ AppState.usePool appState (dqTransaction dqIsoLevel dqTxMode $ runExceptT dqDbHandler)
let eitherResp = join $ mapLeft (Error.PgErr . Error.PgError (Just authRole /= configDbAnonRole)) dbRes let eitherResp = mapLeft Error.PgErr . mapLeft (Error.PgError (Just authRole /= configDbAnonRole)) $ dbRes
when (configLogQuery /= LogQueryDisabled) $ whenLeft eitherResp $ logSQL . Error.status
-- TODO: we use obsQuery twice, one here and one below because in case of an error with the usePool above, the request will finish here and return an error message. liftEither eitherResp >>= liftEither
-- This is because of a combination of ExceptT + our Error module which has Wai.responseLBS.
-- This needs refactoring so only the below obsQuery is used.
lift $ whenLeft eitherResp $ obsQuery . Error.status
liftEither eitherResp
(respTime, resp) <- withTiming $ do (respTime, resp) <- withTiming $ do
let response = Response.actionResponse txResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache let response = Response.actionResponse queryResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache iSchema iNegotiatedByProfile
status' = either Error.status Response.pgrstStatus response when (configLogQuery /= LogQueryDisabled) $ logSQL $ either Error.status Response.pgrstStatus response
-- TODO: see above obsQuery, only this obsQuery should remain after refactoring (because the QueryObs depends on the status)
lift $ obsQuery status'
liftEither response liftEither response
return $ toWaiResponse (ServerTiming jwtTime parseTime planTime txTime respTime) resp return $ toWaiResponse (ServerTiming jwtTime parseTime planTime queryTime respTime) resp
where where
toWaiResponse :: ServerTiming -> Response.PgrstResponse -> Wai.Response toWaiResponse :: ServerTiming -> Response.PgrstResponse -> Wai.Response
toWaiResponse timing (Response.PgrstResponse st hdrs bod) = toWaiResponse timing (Response.PgrstResponse st hdrs bod) = Wai.responseLBS st (hdrs ++ ([serverTimingHeader timing | configServerTimingEnabled])) bod
Wai.responseLBS st (hdrs ++ serverTimingHeaders timing ++ [varyHeader | not $ varyHeaderPresent hdrs]) bod
serverTimingHeaders :: ServerTiming -> [HTTP.Header]
serverTimingHeaders timing = [serverTimingHeader timing | configServerTimingEnabled]
varyHeader :: HTTP.Header
varyHeader = (HTTP.hVary, "Accept, Prefer, Range")
varyHeaderPresent :: [HTTP.Header] -> Bool
varyHeaderPresent = any (\(h, _v) -> h == HTTP.hVary)
withTiming :: Handler IO a -> Handler IO (Maybe Double, a) withTiming :: Handler IO a -> Handler IO (Maybe Double, a)
withTiming f = if configServerTimingEnabled withTiming f = if configServerTimingEnabled
@@ -250,39 +199,3 @@ addRetryHint delay response = do
isServiceUnavailable :: Wai.Response -> Bool isServiceUnavailable :: Wai.Response -> Bool
isServiceUnavailable response = Wai.responseStatus response == HTTP.status503 isServiceUnavailable response = Wai.responseStatus response == HTTP.status503
type AppSockets = (NS.Socket, Maybe NS.Socket)
initSockets :: AppConfig -> IO AppSockets
initSockets AppConfig{..} = do
let
cfg'usp = configServerUnixSocket
cfg'uspm = configServerUnixSocketMode
cfg'host = configServerHost
cfg'port = configServerPort
cfg'adminHost = configAdminServerHost
cfg'adminPort = configAdminServerPort
sock <- case cfg'usp of
-- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows,
-- but we need to have runtime error if we try to use it in Windows, not compile time error
Just path -> createAndBindDomainSocket path cfg'uspm
Nothing -> do
(_, sock) <-
if cfg'port /= 0
then do
sock <- bindPortTCP cfg'port (fromString $ T.unpack cfg'host)
pure (cfg'port, sock)
else do
-- explicitly bind to a random port, returning bound port number
(num, sock) <- bindRandomPortTCP (fromString $ T.unpack cfg'host)
pure (num, sock)
pure sock
adminSock <- case cfg'adminPort of
Just adminPort -> do
adminSock <- bindPortTCP adminPort (fromString $ T.unpack cfg'adminHost)
pure $ Just adminSock
Nothing -> pure Nothing
pure (sock, adminSock)
+139 -93
View File
@@ -1,7 +1,6 @@
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE RecursiveDo #-}
module PostgREST.AppState module PostgREST.AppState
( AppState ( AppState
@@ -14,9 +13,11 @@ module PostgREST.AppState
, getNextListenerDelay , getNextListenerDelay
, getTime , getTime
, getJwtCacheState , getJwtCacheState
, getSocketREST
, getSocketAdmin
, init , init
, initSockets
, initWithPool , initWithPool
, putConfig -- For tests TODO refactoring
, putNextListenerDelay , putNextListenerDelay
, putSchemaCache , putSchemaCache
, putPgVersion , putPgVersion
@@ -31,21 +32,24 @@ module PostgREST.AppState
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import Data.Either.Combinators (whenLeft) import Data.Either.Combinators (whenLeft)
import qualified Data.Text as T (unpack)
import qualified Hasql.Pool as SQL import qualified Hasql.Pool as SQL
import qualified Hasql.Pool.Config as SQL import qualified Hasql.Pool.Config as SQL
import qualified Hasql.Session as SQL import qualified Hasql.Session as SQL
import qualified Hasql.Transaction.Sessions as SQL import qualified Hasql.Transaction.Sessions as SQL
import qualified Network.HTTP.Types.Status as HTTP import qualified Network.HTTP.Types.Status as HTTP
import qualified Network.Socket as NS
import qualified PostgREST.Auth.JwtCache as JwtCache import qualified PostgREST.Auth.JwtCache as JwtCache
import qualified PostgREST.Error as Error import qualified PostgREST.Error as Error
import qualified PostgREST.Logger as Logger import qualified PostgREST.Logger as Logger
import qualified PostgREST.Metrics as Metrics import qualified PostgREST.Metrics as Metrics
import PostgREST.Observation import PostgREST.Observation
import PostgREST.TimeIt (timeItT)
import PostgREST.Version (prettyVersion) import PostgREST.Version (prettyVersion)
import System.TimeIt (timeItT)
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate, import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
updateAction) updateAction)
import Control.Debounce
import Control.Retry (RetryPolicy, RetryStatus (..), capDelay, import Control.Retry (RetryPolicy, RetryStatus (..), capDelay,
exponentialBackoff, retrying, exponentialBackoff, retrying,
rsPreviousDelay) rsPreviousDelay)
@@ -53,21 +57,23 @@ import Data.IORef (IORef, atomicWriteIORef, newIORef,
readIORef) readIORef)
import Data.Time.Clock (UTCTime, getCurrentTime) import Data.Time.Clock (UTCTime, getCurrentTime)
import PostgREST.Auth.JwtCache (JwtCacheState, update) import PostgREST.Auth.JwtCache (JwtCacheState)
import PostgREST.Config (AppConfig (..), import PostgREST.Config (AppConfig (..),
readAppConfig, addFallbackAppName,
toConnectionSettings) readAppConfig)
import PostgREST.Config.Database (queryDbSettings, import PostgREST.Config.Database (queryDbSettings,
queryPgVersion, queryPgVersion,
queryRoleSettings) queryRoleSettings)
import PostgREST.Config.PgVersion (PgVersion (..), import PostgREST.Config.PgVersion (PgVersion (..),
minimumPgVersion) minimumPgVersion)
import PostgREST.Debounce (makeDebouncer)
import PostgREST.SchemaCache (SchemaCache (..), import PostgREST.SchemaCache (SchemaCache (..),
querySchemaCache, querySchemaCache,
showSummary) showSummary)
import PostgREST.SchemaCache.Identifiers (quoteQi) import PostgREST.SchemaCache.Identifiers (dumpQi)
import PostgREST.Unix (createAndBindDomainSocket)
import Data.Streaming.Network (bindPortTCP, bindRandomPortTCP)
import Data.String (IsString (..))
import Protolude import Protolude
data AppState = AppState data AppState = AppState
@@ -78,7 +84,7 @@ data AppState = AppState
-- | Schema cache -- | Schema cache
, stateSchemaCache :: IORef (Maybe SchemaCache) , stateSchemaCache :: IORef (Maybe SchemaCache)
-- | The schema cache status -- | The schema cache status
, stateSCacheStatus :: SchemaCacheStatus , stateSCacheStatus :: IORef SchemaCacheStatus
-- | State of the LISTEN channel -- | State of the LISTEN channel
, stateIsListenerOn :: IORef Bool , stateIsListenerOn :: IORef Bool
-- | starts the connection worker with a debounce -- | starts the connection worker with a debounce
@@ -93,6 +99,10 @@ data AppState = AppState
, stateNextDelay :: IORef Int , stateNextDelay :: IORef Int
-- | Keeps track of the next delay for the listener -- | Keeps track of the next delay for the listener
, stateNextListenerDelay :: IORef Int , stateNextListenerDelay :: IORef Int
-- | Network socket for REST API
, stateSocketREST :: NS.Socket
-- | Network socket for the admin UI
, stateSocketAdmin :: Maybe NS.Socket
-- | Observation handler -- | Observation handler
, stateObserver :: ObservationHandler , stateObserver :: ObservationHandler
-- | JWT Cache -- | JWT Cache
@@ -101,11 +111,13 @@ data AppState = AppState
, stateMetrics :: Metrics.MetricsState , stateMetrics :: Metrics.MetricsState
} }
-- | Schema cache status. -- | Schema cache status
-- Empty means pending and full means loaded. data SchemaCacheStatus
newtype SchemaCacheStatus = SchemaCacheStatus = SCLoaded
{ getSCStatusMVar :: MVar () | SCPending
} deriving Eq
type AppSockets = (NS.Socket, Maybe NS.Socket)
init :: AppConfig -> IO AppState init :: AppConfig -> IO AppState
init conf@AppConfig{configLogLevel, configDbPoolSize} = do init conf@AppConfig{configLogLevel, configDbPoolSize} = do
@@ -115,75 +127,109 @@ init conf@AppConfig{configLogLevel, configDbPoolSize} = do
observer $ AppStartObs prettyVersion observer $ AppStartObs prettyVersion
jwtCacheState <- JwtCache.init
pool <- initPool conf observer pool <- initPool conf observer
initWithPool pool conf loggerState metricsState observer (sock, adminSock) <- initSockets conf
state' <- initWithPool (sock, adminSock) pool conf jwtCacheState loggerState metricsState observer
pure state' { stateSocketREST = sock, stateSocketAdmin = adminSock}
initWithPool :: SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState initWithPool :: AppSockets -> SQL.Pool -> AppConfig -> JwtCache.JwtCacheState -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState
initWithPool pool conf loggerState metricsState observer = mdo initWithPool (sock, adminSock) pool conf jwtCacheState loggerState metricsState observer = do
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
<*> newIORef Nothing <*> newIORef Nothing
<*> newSchemaCacheStatus <*> newIORef SCPending
<*> newIORef False <*> newIORef False
<*> makeDebouncer (retryingSchemaCacheLoad appState *> threadDelay 100000) -- 100ms cooldown <*> pure (pure ())
<*> newIORef conf <*> newIORef conf
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime } <*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }
<*> myThreadId <*> myThreadId
<*> newIORef 0 <*> newIORef 0
<*> newIORef 1 <*> newIORef 1
<*> pure sock
<*> pure adminSock
<*> pure observer <*> pure observer
<*> JwtCache.init conf observer <*> pure jwtCacheState
<*> pure loggerState <*> pure loggerState
<*> pure metricsState <*> pure metricsState
return appState deb <-
let decisecond = 100000 in
mkDebounce defaultDebounceSettings
{ debounceAction = retryingSchemaCacheLoad appState
, debounceFreq = decisecond
, debounceEdge = leadingEdge -- runs the worker at the start and the end
}
return appState { debouncedSCacheLoader = deb}
destroy :: AppState -> IO () destroy :: AppState -> IO ()
destroy = destroyPool destroy = destroyPool
initSockets :: AppConfig -> IO AppSockets
initSockets AppConfig{..} = do
let
cfg'usp = configServerUnixSocket
cfg'uspm = configServerUnixSocketMode
cfg'host = configServerHost
cfg'port = configServerPort
cfg'adminHost = configAdminServerHost
cfg'adminPort = configAdminServerPort
sock <- case cfg'usp of
-- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows,
-- but we need to have runtime error if we try to use it in Windows, not compile time error
Just path -> createAndBindDomainSocket path cfg'uspm
Nothing -> do
(_, sock) <-
if cfg'port /= 0
then do
sock <- bindPortTCP cfg'port (fromString $ T.unpack cfg'host)
pure (cfg'port, sock)
else do
-- explicitly bind to a random port, returning bound port number
(num, sock) <- bindRandomPortTCP (fromString $ T.unpack cfg'host)
pure (num, sock)
pure sock
adminSock <- case cfg'adminPort of
Just adminPort -> do
adminSock <- bindPortTCP adminPort (fromString $ T.unpack cfg'adminHost)
pure $ Just adminSock
Nothing -> pure Nothing
pure (sock, adminSock)
initPool :: AppConfig -> ObservationHandler -> IO SQL.Pool initPool :: AppConfig -> ObservationHandler -> IO SQL.Pool
initPool cfg@AppConfig{..} observer = do initPool AppConfig{..} observer = do
SQL.acquire $ SQL.settings SQL.acquire $ SQL.settings
[ SQL.size configDbPoolSize [ SQL.size configDbPoolSize
, SQL.acquisitionTimeout $ fromIntegral configDbPoolAcquisitionTimeout , SQL.acquisitionTimeout $ fromIntegral configDbPoolAcquisitionTimeout
, SQL.agingTimeout $ fromIntegral configDbPoolMaxLifetime , SQL.agingTimeout $ fromIntegral configDbPoolMaxLifetime
, SQL.idlenessTimeout $ fromIntegral configDbPoolMaxIdletime , SQL.idlenessTimeout $ fromIntegral configDbPoolMaxIdletime
, SQL.staticConnectionSettings $ toConnectionSettings identity cfg , SQL.staticConnectionSettings (toUtf8 $ addFallbackAppName prettyVersion configDbUri)
, SQL.observationHandler $ observer . HasqlPoolObs , SQL.observationHandler $ observer . HasqlPoolObs
] ]
-- | Run an action with a database connection. -- | Run an action with a database connection.
usePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a) usePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a)
usePool AppState{stateObserver=observer, stateMainThreadId=mainThreadId, ..} sess = do usePool AppState{stateObserver=observer, stateMainThreadId=mainThreadId, ..} sess = do
observer PoolRequest observer PoolRequest
res <- SQL.use statePool sess res <- SQL.use statePool sess
observer PoolRequestFullfilled observer PoolRequestFullfilled
whenLeft res (\case whenLeft res (\case
SQL.AcquisitionTimeoutUsageError -> SQL.AcquisitionTimeoutUsageError ->
observer PoolAcqTimeoutObs observer $ PoolAcqTimeoutObs SQL.AcquisitionTimeoutUsageError
err@(SQL.ConnectionUsageError e) -> err@(SQL.ConnectionUsageError e) ->
let failureMessage = BS.unpack $ fromMaybe mempty e in let failureMessage = BS.unpack $ fromMaybe mempty e in
when (("FATAL: password authentication failed" `isInfixOf` failureMessage) || ("no password supplied" `isInfixOf` failureMessage)) $ do when (("FATAL: password authentication failed" `isInfixOf` failureMessage) || ("no password supplied" `isInfixOf` failureMessage)) $ do
observer $ ExitDBFatalError ServerAuthError err observer $ ExitDBFatalError ServerAuthError err
killThread mainThreadId killThread mainThreadId
err@(SQL.SessionUsageError (SQL.QueryError tpl _ (SQL.ResultError resultErr))) -> err@(SQL.SessionUsageError (SQL.QueryError tpl _ (SQL.ResultError resultErr))) -> do
handleResultError err tpl resultErr
err@(SQL.SessionUsageError (SQL.PipelineError (SQL.ResultError resultErr))) ->
-- Passing the empty template will not work for schema cache queries, see TODO further below.
handleResultError err mempty resultErr
err@(SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _))) ->
-- An error on the client-side, usually indicates problems with connection
observer $ QueryErrorCodeHighObs err
SQL.SessionUsageError (SQL.PipelineError (SQL.ClientError _)) -> pure ()
)
return res
where
handleResultError err tpl resultErr = do
case resultErr of case resultErr of
SQL.UnexpectedResult{} -> do SQL.UnexpectedResult{} -> do
observer $ ExitDBFatalError ServerPgrstBug err observer $ ExitDBFatalError ServerPgrstBug err
@@ -216,17 +262,19 @@ 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
err@(SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _))) ->
-- An error on the client-side, usually indicates problems wth connection
observer $ QueryErrorCodeHighObs err
)
return res
-- | Flush the connection pool so that any future use of the pool will -- | Flush the connection pool so that any future use of the pool will
-- use connections freshly established after this call. -- use connections freshly established after this call.
-- | Emits PoolFlushed observation
flushPool :: AppState -> IO () flushPool :: AppState -> IO ()
flushPool AppState{..} = do flushPool AppState{..} = SQL.release statePool
SQL.release statePool
stateObserver PoolFlushed
-- | Destroy the pool on shutdown. -- | Destroy the pool on shutdown.
-- | Differs from flushPool in not emiting PoolFlushed observation.
destroyPool :: AppState -> IO () destroyPool :: AppState -> IO ()
destroyPool AppState{..} = SQL.release statePool destroyPool AppState{..} = SQL.release statePool
@@ -266,6 +314,12 @@ getTime = stateGetTime
getJwtCacheState :: AppState -> JwtCacheState getJwtCacheState :: AppState -> JwtCacheState
getJwtCacheState = stateJwtCache getJwtCacheState = stateJwtCache
getSocketREST :: AppState -> NS.Socket
getSocketREST = stateSocketREST
getSocketAdmin :: AppState -> Maybe NS.Socket
getSocketAdmin = stateSocketAdmin
getMainThreadId :: AppState -> ThreadId getMainThreadId :: AppState -> ThreadId
getMainThreadId = stateMainThreadId getMainThreadId = stateMainThreadId
@@ -282,15 +336,18 @@ putIsListenerOn = atomicWriteIORef . stateIsListenerOn
isLoaded :: AppState -> IO Bool isLoaded :: AppState -> IO Bool
isLoaded x = do isLoaded x = do
scacheLoaded <- isSchemaCacheLoaded x scacheStatus <- readIORef $ stateSCacheStatus x
connEstablished <- isConnEstablished x connEstablished <- isConnEstablished x
return $ scacheLoaded && connEstablished return $ scacheStatus == SCLoaded && connEstablished
isPending :: AppState -> IO Bool isPending :: AppState -> IO Bool
isPending x = do isPending x = do
scacheLoaded <- isSchemaCacheLoaded x scacheStatus <- readIORef $ stateSCacheStatus x
connEstablished <- isConnEstablished x connEstablished <- isConnEstablished x
return $ not scacheLoaded || not connEstablished return $ scacheStatus == SCPending || not connEstablished
putSCacheStatus :: AppState -> SchemaCacheStatus -> IO ()
putSCacheStatus = atomicWriteIORef . stateSCacheStatus
getObserver :: AppState -> ObservationHandler getObserver :: AppState -> ObservationHandler
getObserver = stateObserver getObserver = stateObserver
@@ -308,6 +365,9 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
when (rsIterNumber > 0) $ do when (rsIterNumber > 0) $ do
let delay = fromMaybe 0 rsPreviousDelay `div` oneSecondInUs let delay = fromMaybe 0 rsPreviousDelay `div` oneSecondInUs
observer $ ConnectionRetryObs delay observer $ ConnectionRetryObs delay
putNextListenerDelay appState delay
flushPool appState
(,) <$> qPgVersion <*> (qInDbConfig *> qSchemaCache) (,) <$> qPgVersion <*> (qInDbConfig *> qSchemaCache)
) )
@@ -315,7 +375,7 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
qPgVersion :: IO (Maybe PgVersion) qPgVersion :: IO (Maybe PgVersion)
qPgVersion = do qPgVersion = do
AppConfig{..} <- getConfig appState AppConfig{..} <- getConfig appState
pgVersion <- usePool appState queryPgVersion pgVersion <- usePool appState (queryPgVersion False) -- No need to prepare the query here, as the connection might not be established
case pgVersion of case pgVersion of
Left e -> do Left e -> do
observer $ QueryPgVersionError e observer $ QueryPgVersionError e
@@ -323,16 +383,14 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
observer ExitDBNoRecoveryObs observer ExitDBNoRecoveryObs
killThread mainThreadId killThread mainThreadId
return Nothing return Nothing
Right actualPgVersion -> Right actualPgVersion -> do
if actualPgVersion < minimumPgVersion then do when (actualPgVersion < minimumPgVersion) $ do
observer $ ExitUnsupportedPgVersion actualPgVersion minimumPgVersion observer $ ExitUnsupportedPgVersion actualPgVersion minimumPgVersion
killThread mainThreadId killThread mainThreadId
return Nothing observer $ DBConnectedObs $ pgvFullName actualPgVersion
else do observer $ PoolInit configDbPoolSize
observer $ DBConnectedObs $ pgvFullName actualPgVersion putPgVersion appState actualPgVersion
observer $ PoolInit configDbPoolSize return $ Just actualPgVersion
putPgVersion appState actualPgVersion
return $ Just actualPgVersion
qInDbConfig :: IO () qInDbConfig :: IO ()
qInDbConfig = do qInDbConfig = do
@@ -343,27 +401,24 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
qSchemaCache = do qSchemaCache = do
conf@AppConfig{..} <- getConfig appState conf@AppConfig{..} <- getConfig appState
(resultTime, result) <- (resultTime, result) <-
timeItT $ usePool appState (SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ querySchemaCache conf) let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in
timeItT $ usePool appState (transaction SQL.ReadCommitted SQL.Read $ querySchemaCache conf)
case result of case result of
Left e -> do Left e -> do
markSchemaCachePending appState putSCacheStatus appState SCPending
putSchemaCache appState Nothing putSchemaCache appState Nothing
observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e
return Nothing return Nothing
Right sCache -> do Right sCache -> do
-- IMPORTANT: While the pending schema cache state starts from running the above querySchemaCache, only at this stage we block API requests due to the usage of an -- IMPORTANT: While the pending schema cache state starts from running the above querySchemaCache, only at this stage we block API requests due to the usage of an
-- IORef on putSchemaCache. This is why schema cache status is marked as pending here to signal the Admin server (using isPending) that we're on a recovery state. -- IORef on putSchemaCache. This is why SCacheStatus is put at SCPending here to signal the Admin server (using isPending) that we're on a recovery state.
markSchemaCachePending appState putSCacheStatus appState SCPending
putSchemaCache appState $ Just sCache putSchemaCache appState $ Just sCache
(loadTime, summary) <- timeItT (evaluate $ showSummary sCache) observer $ SchemaCacheQueriedObs resultTime
-- Flush the pool after loading the schema cache to reset any stale session cache entries (t, _) <- timeItT $ observer $ SchemaCacheSummaryObs $ showSummary sCache
-- We do it after successfully querying the schema cache (because this can fail and during retries we would flush the pool repeatedly unnecessarily) observer $ SchemaCacheLoadedObs t
-- and after marking sCacheStatus as pending, putSCacheStatus appState SCLoaded
flushPool appState
observer $ SchemaCacheQueriedObs resultTime $ dbQueryTimings sCache
observer $ SchemaCacheLoadedObs loadTime summary
markSchemaCacheLoaded appState
return $ Just sCache return $ Just sCache
shouldRetry :: RetryStatus -> (Maybe PgVersion, Maybe SchemaCache) -> IO Bool shouldRetry :: RetryStatus -> (Maybe PgVersion, Maybe SchemaCache) -> IO Bool
@@ -379,18 +434,6 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
oneSecondInUs = 1000000 -- one second in microseconds oneSecondInUs = 1000000 -- one second in microseconds
newSchemaCacheStatus :: IO SchemaCacheStatus
newSchemaCacheStatus = SchemaCacheStatus <$> newEmptyMVar
markSchemaCachePending :: AppState -> IO ()
markSchemaCachePending = void . tryTakeMVar . getSCStatusMVar . stateSCacheStatus
markSchemaCacheLoaded :: AppState -> IO ()
markSchemaCacheLoaded = void . (`tryPutMVar` ()) . getSCStatusMVar . stateSCacheStatus
isSchemaCacheLoaded :: AppState -> IO Bool
isSchemaCacheLoaded = fmap not . isEmptyMVar . getSCStatusMVar . stateSCacheStatus
-- | 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 ()
@@ -399,7 +442,7 @@ readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
pgVer <- getPgVersion appState pgVer <- getPgVersion appState
dbSettings <- dbSettings <-
if configDbConfig conf then do if configDbConfig conf then do
qDbSettings <- usePool appState (queryDbSettings (quoteQi <$> configDbPreConfig conf)) 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
@@ -409,7 +452,7 @@ readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
pure mempty pure mempty
(roleSettings, roleIsolationLvl) <- (roleSettings, roleIsolationLvl) <-
if configDbConfig conf then do if configDbConfig conf then do
rSettings <- usePool appState (queryRoleSettings pgVer) 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
@@ -428,7 +471,10 @@ readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
-- After the config has reloaded, jwt-secret might have changed, so -- After the config has reloaded, jwt-secret might have changed, so
-- if it has changed, it is important to invalidate the jwt cache -- if it has changed, it is important to invalidate the jwt cache
-- entries, because they were cached using the old secret -- entries, because they were cached using the old secret
update (getJwtCacheState appState) newConf if configJwtSecret conf == configJwtSecret newConf then
pass
else
JwtCache.emptyCache (getJwtCacheState appState) -- atomic O(1) operation
if startingUp then if startingUp then
pass pass
+151 -17
View File
@@ -1,4 +1,3 @@
{-# LANGUAGE RecordWildCards #-}
{-| {-|
Module : PostgREST.Auth Module : PostgREST.Auth
Description : PostgREST authentication functions. Description : PostgREST authentication functions.
@@ -11,6 +10,8 @@ Authentication should always be implemented in an external service.
In the test suite there is an example of simple login function that can be used for a In the test suite there is an example of simple login function that can be used for a
very simple authentication system inside the PostgreSQL database. very simple authentication system inside the PostgreSQL database.
-} -}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
module PostgREST.Auth module PostgREST.Auth
( getResult ( getResult
, getJwtDur , getJwtDur
@@ -18,44 +19,177 @@ module PostgREST.Auth
, middleware , middleware
) where ) where
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Key as K
import qualified Data.Aeson.KeyMap as KM
import qualified Data.Aeson.Types as JSON
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.ByteString.Internal as BS
import qualified Data.ByteString.Lazy.Char8 as LBS
import qualified Data.Scientific as Sci
import qualified Data.Text as T
import qualified Data.Vault.Lazy as Vault import qualified Data.Vault.Lazy as Vault
import qualified Data.Vector as V
import qualified Jose.Jwk as JWT
import qualified Jose.Jwt as JWT
import qualified Network.HTTP.Types.Header as HTTP import qualified Network.HTTP.Types.Header as HTTP
import qualified Network.Wai as Wai import qualified Network.Wai as Wai
import qualified Network.Wai.Middleware.HttpAuth as Wai import qualified Network.Wai.Middleware.HttpAuth as Wai
import Data.List (lookup) import Control.Monad.Except (liftEither)
import PostgREST.TimeIt (timeItT) import Data.Either.Combinators (mapLeft)
import System.IO.Unsafe (unsafePerformIO) import Data.List (lookup)
import Data.Time.Clock (UTCTime, nominalDiffTimeToSeconds)
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
import System.IO.Unsafe (unsafePerformIO)
import System.TimeIt (timeItT)
import PostgREST.AppState (AppState, getConfig, getJwtCacheState, import PostgREST.AppState (AppState, getConfig, getJwtCacheState,
getTime) getTime)
import PostgREST.Auth.Jwt (parseClaims)
import PostgREST.Auth.JwtCache (lookupJwtCache) import PostgREST.Auth.JwtCache (lookupJwtCache)
import PostgREST.Auth.Types (AuthResult (..)) import PostgREST.Auth.Types (AuthResult (..))
import PostgREST.Config (AppConfig (..)) import PostgREST.Config (AppConfig (..), FilterExp (..),
import PostgREST.Error (Error (..)) JSPath, JSPathExp (..))
import PostgREST.Error (Error (..), JwtError (..))
import Protolude import Protolude
-- | Validate authorization header -- | Receives the JWT secret and audience (from config) and a JWT and returns a
-- 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 AppConfig{..} (Just tkn) time = do
secret <- liftEither . maybeToRight (JwtErr JwtSecretMissing) $ configJWKS
tknWith3Parts <- liftEither $ hasThreeParts tkn
eitherContent <- liftIO $ JWT.decode (JWT.keys secret) Nothing tknWith3Parts
content <- liftEither . mapLeft (JwtErr . jwtDecodeError) $ eitherContent
liftEither $ mapLeft JwtErr $ verifyClaims content
where
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)
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"
-- Control never reaches here, the decode function only returns the above three
jwtDecodeError _ = JwtDecodeError "JWT couldn't be decoded"
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" >>
return jclaims
_ -> Left $ JwtClaimsError "Parsing claims failed"
-- TODO: We could enable JWE support here (encrypted tokens)
verifyClaims _ = Left $ JwtDecodeError "Unsupported token type"
verifyClaim mclaims claim func err = do
isValid <- maybe (Right True) func (KM.lookup claim mclaims)
unless isValid $ Left $ JwtClaimsError err
allowedSkewSeconds = 30 :: Int64
now = floor . nominalDiffTimeToSeconds $ utcTimeToPOSIXSeconds time
sciToInt = fromMaybe 0 . Sci.toBoundedInteger
allStrings = all (\case (JSON.String _) -> True; _ -> False)
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"
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"
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"
isValidAudClaim :: JSON.Value -> Either JwtError Bool
isValidAudClaim JSON.Null = Right True -- {"aud": null} is valid for all audiences
isValidAudClaim (JSON.String str) = Right $ maybe (const True) (==) configJwtAudience str
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"
parseClaims :: Monad m =>
AppConfig -> JSON.Value -> ExceptT Error m AuthResult
parseClaims AppConfig{..} jclaims@(JSON.Object mclaims) = do
-- role defaults to anon if not specified in jwt
role <- liftEither . maybeToRight (JwtErr JwtTokenRequired) $
unquoted <$> walkJSPath (Just jclaims) configJwtRoleClaimKey <|> configDbAnonRole
return AuthResult
{ authClaims = mclaims & KM.insert "role" (JSON.toJSON $ decodeUtf8 role)
, authRole = role
}
where
walkJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value
walkJSPath x [] = x
walkJSPath (Just (JSON.Object o)) (JSPKey key:rest) = walkJSPath (KM.lookup (K.fromText key) o) rest
walkJSPath (Just (JSON.Array ar)) (JSPIdx idx:rest) = walkJSPath (ar V.!? idx) rest
walkJSPath (Just (JSON.Array ar)) [JSPFilter (EqualsCond txt)] = findFirstMatch (==) txt ar
walkJSPath (Just (JSON.Array ar)) [JSPFilter (NotEqualsCond txt)] = findFirstMatch (/=) txt ar
walkJSPath (Just (JSON.Array ar)) [JSPFilter (StartsWithCond txt)] = findFirstMatch T.isPrefixOf txt ar
walkJSPath (Just (JSON.Array ar)) [JSPFilter (EndsWithCond txt)] = findFirstMatch T.isSuffixOf txt ar
walkJSPath (Just (JSON.Array ar)) [JSPFilter (ContainsCond txt)] = findFirstMatch T.isInfixOf txt ar
walkJSPath _ _ = Nothing
findFirstMatch matchWith pattern = foldr checkMatch Nothing
where
checkMatch (JSON.String txt) acc
| pattern `matchWith` txt = Just $ JSON.String txt
| otherwise = acc
checkMatch _ acc = acc
unquoted :: JSON.Value -> BS.ByteString
unquoted (JSON.String t) = encodeUtf8 t
unquoted v = LBS.toStrict $ JSON.encode v
-- impossible case - just added to please -Wincomplete-patterns
parseClaims _ _ = return AuthResult { authClaims = KM.empty, authRole = mempty }
-- | Validate authorization header.
-- Parse and store JWT claims for future use in the request. -- Parse and store JWT claims for future use in the request.
middleware :: AppState -> Wai.Middleware middleware :: AppState -> Wai.Middleware
middleware appState app req respond = do middleware appState app req respond = do
conf@AppConfig{..} <- getConfig appState conf <- getConfig appState
time <- getTime appState time <- getTime appState
let token = Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req) let token = Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req)
parseJwt = runExceptT $ lookupJwtCache jwtCacheState token >>= parseClaims conf time parseJwt = runExceptT $ parseToken conf token time >>= parseClaims conf
jwtCacheState = getJwtCacheState appState jwtCacheState = getJwtCacheState appState
-- If ServerTimingEnabled -> calculate JWT validation time -- If ServerTimingEnabled -> calculate JWT validation time
req' <- if configServerTimingEnabled then do -- If JwtCacheMaxLifetime -> cache JWT validation result
(dur, authResult) <- timeItT parseJwt req' <- case (configServerTimingEnabled conf, configJwtCacheMaxLifetime conf) of
pure $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur } (True, 0) -> do
else do (dur, authResult) <- timeItT parseJwt
authResult <- parseJwt return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
pure $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
(True, maxLifetime) -> do
(dur, authResult) <- timeItT $ case token of
Just tkn -> lookupJwtCache jwtCacheState tkn maxLifetime parseJwt time
Nothing -> parseJwt
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
(False, 0) -> do
authResult <- parseJwt
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
(False, maxLifetime) -> do
authResult <- case token of
Just tkn -> lookupJwtCache jwtCacheState tkn maxLifetime parseJwt time
Nothing -> parseJwt
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
app req' respond app req' respond
-125
View File
@@ -1,125 +0,0 @@
{-|
Module : PostgREST.Auth.Jwt
Description : PostgREST JWT support functions.
This module provides functions to deal with JWT parsing and validation (http://jwt.io).
-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE QuantifiedConstraints #-}
module PostgREST.Auth.Jwt
( parseAndDecodeClaims
, parseClaims) where
import qualified Data.Aeson as JSON
import qualified Data.ByteString as BS
import qualified Data.ByteString.Internal as BS
import qualified Data.ByteString.Lazy.Char8 as LBS
import qualified Data.Scientific as Sci
import qualified Jose.Jwk as JWT
import qualified Jose.Jwt as JWT
import Control.Monad.Except (liftEither)
import Data.Either.Combinators (mapLeft)
import Data.Text ()
import Data.Time.Clock (UTCTime, nominalDiffTimeToSeconds)
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
import PostgREST.Auth.Types (AuthResult (..))
import PostgREST.Config (AppConfig (..), audMatchesCfg)
import PostgREST.Config.JSPath (walkJSPath)
import PostgREST.Error (Error (..), JwtClaimsError (..),
JwtDecodeError (..), JwtError (..))
import Data.Aeson ((.:?))
import Data.Aeson.Types (parseMaybe)
import Jose.Jwk (JwkSet)
import Protolude hiding (first)
parseAndDecodeClaims :: (MonadError Error m, MonadIO m) => JwkSet -> ByteString -> m JSON.Object
parseAndDecodeClaims jwkSet token = parseToken jwkSet token >>= decodeClaims
decodeClaims :: MonadError Error m => JWT.JwtContent -> m JSON.Object
decodeClaims (JWT.Jws (_, claims)) = maybe (throwError (JwtErr $ JwtClaimsErr ParsingClaimsFailed)) pure (JSON.decodeStrict claims)
decodeClaims _ = throwError $ JwtErr $ JwtDecodeErr UnsupportedTokenType
validateClaims :: MonadError Error m => UTCTime -> (Text -> Bool) -> JSON.Object -> m ()
validateClaims time audMatches claims = liftEither $ maybeToLeft () (fmap JwtErr . getAlt $ JwtClaimsErr <$> checkForErrors time audMatches claims)
data ValidAud = VAString Text | VAArray [Text] deriving Generic
instance JSON.FromJSON ValidAud where
parseJSON = JSON.genericParseJSON JSON.defaultOptions { JSON.sumEncoding = JSON.UntaggedValue }
checkForErrors :: (Applicative m, Monoid (m JwtClaimsError)) => UTCTime -> (Text -> Bool) -> JSON.Object -> m JwtClaimsError
checkForErrors time audMatches = mconcat
[
claim "exp" ExpClaimNotNumber $ inThePast JWTExpired
, claim "nbf" NbfClaimNotNumber $ inTheFuture JWTNotYetValid
, claim "iat" IatClaimNotNumber $ inTheFuture JWTIssuedAtFuture
, claim "aud" AudClaimNotStringOrArray $ checkValue (not . validAud) JWTNotInAudience
]
where
allowedSkewSeconds = 30 :: Int64
sciToInt = fromMaybe 0 . Sci.toBoundedInteger
toSec = floor . nominalDiffTimeToSeconds . utcTimeToPOSIXSeconds
now = toSec time
inTheFuture = checkTime ((now + allowedSkewSeconds) <)
inThePast = checkTime ((now - allowedSkewSeconds) >)
checkTime cond = checkValue (cond. sciToInt)
validAud = \case
(VAString aud) -> audMatches aud
(VAArray auds) -> null auds || any audMatches auds
checkValue invalid msg val =
if invalid val then
pure msg
else
mempty
claim key parseError checkParsed = maybe (pure parseError) (maybe mempty checkParsed) . parseMaybe (.:? key)
-- | Receives the JWT secret and audience (from config) and a JWT and returns a
-- JSON object of JWT claims.
parseToken :: (MonadError Error m, MonadIO m) => JwkSet -> ByteString -> m JWT.JwtContent
parseToken _ "" = throwError $ JwtErr $ JwtDecodeErr EmptyAuthHeader
parseToken secret tkn = do
tknWith3Parts <- hasThreeParts tkn
eitherContent <- liftIO $ JWT.decode (JWT.keys secret) Nothing tknWith3Parts
liftEither . mapLeft (JwtErr . jwtDecodeError) $ eitherContent
where
hasThreeParts token = case length $ BS.split (BS.c2w '.') token of
3 -> pure token
n -> throwError $ 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 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 _ = JwtDecodeErr UnreachableDecodeError
parseClaims :: (MonadError Error m, MonadIO m) => AppConfig -> UTCTime -> JSON.Object -> m AuthResult
parseClaims cfg@AppConfig{configJwtRoleClaimKey, configDbAnonRole} time mclaims = do
validateClaims time (audMatchesCfg cfg) mclaims
-- role defaults to anon if not specified in jwt
role <- liftEither . maybeToRight (JwtErr JwtTokenRequired) $
unquoted <$> walkJSPath (Just $ JSON.Object mclaims) configJwtRoleClaimKey <|> configDbAnonRole
pure AuthResult
{ authClaims = mclaims
, authRole = role
}
where
unquoted :: JSON.Value -> BS.ByteString
unquoted (JSON.String t) = encodeUtf8 t
unquoted v = LBS.toStrict $ JSON.encode v

Some files were not shown because too many files have changed in this diff Show More